├── .gitignore ├── .jshintrc ├── .senv ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── MAINTAINING.md ├── Makefile ├── README.md ├── bower.json ├── examples └── index.html ├── fetch.js ├── package.json ├── script ├── phantomjs ├── saucelabs ├── saucelabs-result ├── saucelabs-start ├── saucelabs-status ├── server └── test ├── src └── assert.js └── test ├── .gitignore ├── .jshintrc ├── test-worker.html ├── test.html ├── test.js └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | bower_components/ 3 | node_modules/ 4 | test/assert.js 5 | test/es5shimsham.js 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "es3": true, 5 | "immed": true, 6 | "indent": 2, 7 | "latedef": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "quotmark": true, 11 | "undef": true, 12 | "unused": true, 13 | "strict": true, 14 | "trailing": true, 15 | "asi": true, 16 | "boss": true, 17 | "esnext": true, 18 | "eqnull": true, 19 | "browser": true, 20 | "worker": true 21 | } 22 | -------------------------------------------------------------------------------- /.senv: -------------------------------------------------------------------------------- 1 | hqhBWY7QnNkyaM/nUOnpYMUWhjMyWw8bl6EJOPSxHlgopMEfoIOestV3mhVEwEKTpTnHtx9pv+zl62lYr6kBrVfSaDg7uF+WEMXlMo/HecmE3Qz21JLkBDZYc4JAPvjF9dObU1WSVAHI4uFsVcCrylzulR4DnV3zWum4l+TvJ0rcXBCISeNPLj/YQxyJMDm+QSSDY6Sg1LXNHWUU6ORLKrW4XrIZmL4WxEIm7IXThK7gNslojDlz7krRJ8XIS5nCx/kI7L7ZfJwd3VG+d56kW7a97U1OJcjNXVL1L3OwX+oqKAu4ojOehkTBepmnOtu62LRHd1Jgr07faT++hvL5sA== 2 | SWg+ILWFcdwg0rU+8fYUoZSjxd1MWmRBJGS5EpbFvrD0Lzdv2lGmrfTMfDGjV0nmpnZhSkAXcWJENw7J9XRQAMmbGq93NL8K0Tv1B49xhuIffIkJlmRP3fosIJGrpHBZWa8v/87Jtirqgg58xisjh7LZRrhrHiw3BPeZumv2t7YO1YzAGfcxthcdtxPs+jBl8iufvhO03/oE8Gd7d3FjfnOBDJwyoTcYTNvTXdh85GINJRKhYhSdq1pq+ASDNxIvfEBUhLzwvQsG4ScuDcUpC7T63dfH+BhtakT2EkC/8w5vRw9jK7DM2UevOSnLZroC05CQDawz453X6CxgLlwJJg== 3 | ZaPgL6eMy+Gg1ti+ZYH09tXH+pUA0oEaPMb5vTsxUye0Hqam2zRtJ5AYlZQO1lTp0IuJjFTTuxve07tfy0MPgP9Bntq9YkB06+kRMVJPRB1kSAoq/tTGv1binbe4g8bN9B7LFu1/ODPyNVPEOaisy4WjRBym8t5n59cQFl4keQqqoB+fWUf4DVXN+7Xi84wy1gSanwH3jKCVtOhBzaWIKY+pLk2qYYLUUuKlB5SVk9ywEzFOoStVlZXGemOVVlXrtOg3wW83cce5zAmdbrrQz+nAquPuZepDmVvmg1mDdQ30BQ3p6BJixwyvsBRtdVQAJEr98nRMpnkyylaQNKAlcw== 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: make 3 | script: make test 4 | env: 5 | global: 6 | - SAUCE_USERNAME=github-fetch 7 | - SAUCE_ACCESS_KEY=c3d37f93-0c2e-4834-9da5-eddc0d8c6299 8 | matrix: 9 | - PHANTOMJS=1 10 | - SAUCE_PLATFORM="Windows 7" SAUCE_BROWSER="googlechrome" SAUCE_VERSION="" 11 | - SAUCE_PLATFORM="Windows 7" SAUCE_BROWSER="internet explorer" SAUCE_VERSION="11" 12 | - SAUCE_PLATFORM="Windows 7" SAUCE_BROWSER="internet explorer" SAUCE_VERSION="10" 13 | - SAUCE_PLATFORM="Windows 7" SAUCE_BROWSER="internet explorer" SAUCE_VERSION="9" 14 | addons: 15 | sauce_connect: true 16 | deploy: 17 | provider: npm 18 | email: matt@mattandre.ws 19 | api_key: 20 | secure: FcQZz0HCJhrz8FZyyfkuXj4cwoBP+PeQ7LzOVU1bG7v3kX7Z33n8glD+32QScT2Uu369exdjkk3mzCaCMsfZTTvRm9STnuJIrPtdB2/FwfaWiyJiB1oZ2UCd5UQM0zMiQrtg+gR8FUBBgi3GICOkzAqTbso+C7P2IJtvpP9RTTI= 21 | on: 22 | all_branches: true 23 | tags: true 24 | repo: github/fetch 25 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.5.0 / 2017-02-28 2 | ================== 3 | * Fix self 4 | 5 | 1.4.3 / 2016-06-22 6 | ================== 7 | * Compatibility when defining the global object, merged #5 8 | 9 | 1.4.2 / 2016-03-13 10 | ================== 11 | * Fix request and response content-type different bug 12 | 13 | 1.4.1 / 2016-01-20 14 | ================== 15 | * Make blob supporting different encoding, fixed #1 16 | 17 | 1.4.0 / 2015-12-09 18 | ================== 19 | * Sync with github/fetch 20 | * Fix promise resolve twice bug 21 | 22 | 1.3.1 / 2015-09-08 23 | ================== 24 | * Use console.warn to log messages 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 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 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | ## Releasing a new version 4 | 5 | This project follows [semver](http://semver.org/). So if you are making a bug 6 | fix, only increment the patch level "1.0.x". If any new files are added, a 7 | minor version "1.x.x" bump is in order. 8 | 9 | ### Make a release commit 10 | 11 | To prepare the release commit: 12 | 13 | 1. Edit the [bower.json](https://github.com/github/fetch/blob/master/bower.json) 14 | `version` value. 15 | 2. Change the npm [package.json](https://github.com/github/fetch/blob/master/package.json) 16 | `version` value to match. 17 | 3. Make a single commit with the description as "Fetch 1.x.x". 18 | 4. Finally, tag the commit with `v1.x.x`. 19 | 20 | ``` 21 | $ git pull 22 | $ vim bower.json 23 | $ vim package.json 24 | $ git add bower.json package.json 25 | $ git commit -m "Fetch 1.x.x" 26 | $ git tag v1.x.x 27 | $ git push 28 | $ git push --tags 29 | ``` 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: node_modules/ assert es5shimsham 2 | 3 | es5shimsham: 4 | (curl https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-shim.min.js; echo ''; curl https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-sham.min.js) > test/es5shimsham.js 5 | 6 | test: node_modules/ build lint 7 | ./script/test 8 | 9 | lint: node_modules/ 10 | ./node_modules/.bin/jshint *.js test/*.js 11 | 12 | assert: node_modules/ 13 | ./node_modules/.bin/browserify src/assert.js -s assert > test/assert.js 14 | 15 | node_modules/: 16 | npm install 17 | 18 | clean: 19 | rm -rf ./node_modules 20 | 21 | .PHONY: build clean lint test saucelabs travis 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # window.fetch polyfill 2 | 3 | **This fork supports IE8 with es5-shim, es5-sham and es6-promise.** 4 | 5 | **If you also use JSONP, checkout [fetch-jsonp](https://github.com/camsong/fetch-jsonp).** 6 | 7 | **Fetch API is still very new and not fully supported in some browsers, so you may 8 | need to check browser verson as well as if `window.fetch` exists. In this case, 9 | you can set `window.__disableNativeFetch = true` to use AJAX polyfill always.** 10 | 11 | The global `fetch` function is an easier way to make web requests and handle 12 | responses than using an XMLHttpRequest. This polyfill is written as closely as 13 | possible to the standard Fetch specification at https://fetch.spec.whatwg.org. 14 | 15 | ## Installation 16 | 17 | ```sh 18 | $ npm install fetch-ie8 --save 19 | ``` 20 | 21 | You'll also need a Promise polyfill for older browsers. 22 | 23 | ```sh 24 | $ npm install es6-promise 25 | ``` 26 | 27 | Run this to polyfill the global environment at the beginning of your application. 28 | ```js 29 | require('es6-promise').polyfill(); 30 | ``` 31 | 32 | (For a node.js implementation, try [node-fetch](https://github.com/bitinn/node-fetch)) 33 | 34 | ## Usage 35 | 36 | The `fetch` function supports any HTTP method. We'll focus on GET and POST 37 | example requests. 38 | 39 | ### HTML 40 | 41 | ```javascript 42 | fetch('/users.html') 43 | .then(function(response) { 44 | return response.text() 45 | }).then(function(body) { 46 | document.body.innerHTML = body 47 | }) 48 | ``` 49 | 50 | ### JSON 51 | 52 | ```javascript 53 | fetch('/users.json') 54 | .then(function(response) { 55 | return response.json() 56 | }).then(function(json) { 57 | console.log('parsed json', json) 58 | }).catch(function(ex) { 59 | console.log('parsing failed', ex) 60 | }) 61 | ``` 62 | 63 | ### Response metadata 64 | 65 | ```javascript 66 | fetch('/users.json').then(function(response) { 67 | console.log(response.headers.get('Content-Type')) 68 | console.log(response.headers.get('Date')) 69 | console.log(response.status) 70 | console.log(response.statusText) 71 | }) 72 | ``` 73 | 74 | ### Post form 75 | 76 | ```javascript 77 | var form = document.querySelector('form') 78 | 79 | fetch('/query', { 80 | method: 'post', 81 | body: new FormData(form) 82 | }) 83 | ``` 84 | 85 | ### Post JSON 86 | 87 | ```javascript 88 | fetch('/users', { 89 | method: 'post', 90 | headers: { 91 | 'Accept': 'application/json', 92 | 'Content-Type': 'application/json' 93 | }, 94 | body: JSON.stringify({ 95 | name: 'Hubot', 96 | login: 'hubot', 97 | }) 98 | }) 99 | ``` 100 | 101 | ### File upload 102 | 103 | ```javascript 104 | var input = document.querySelector('input[type="file"]') 105 | 106 | var form = new FormData() 107 | form.append('file', input.files[0]) 108 | form.append('user', 'hubot') 109 | 110 | fetch('/avatars', { 111 | method: 'post', 112 | body: form 113 | }) 114 | ``` 115 | 116 | ### Success and error handlers 117 | 118 | This causes `fetch` to behave like jQuery's `$.ajax` by rejecting the `Promise` 119 | on HTTP failure status codes like 404, 500, etc. The response `Promise` is 120 | resolved only on successful, 200 level, status codes. 121 | 122 | ```javascript 123 | function status(response) { 124 | if (response.status >= 200 && response.status < 300) { 125 | return response 126 | } 127 | throw new Error(response.statusText) 128 | } 129 | 130 | function json(response) { 131 | return response.json() 132 | } 133 | 134 | fetch('/users') 135 | .then(status) 136 | .then(json) 137 | .then(function(json) { 138 | console.log('request succeeded with json response', json) 139 | }).catch(function(error) { 140 | console.log('request failed', error) 141 | }) 142 | ``` 143 | 144 | ### Response URL caveat 145 | 146 | The `Response` object has a URL attribute for the final responded resource. 147 | Usually this is the same as the `Request` url, but in the case of a redirect, 148 | its all transparent. Newer versions of XHR include a `responseURL` attribute 149 | that returns this value. But not every browser supports this. The compromise 150 | requires setting a special server side header to tell the browser what URL it 151 | just requested (yeah, I know browsers). 152 | 153 | ``` ruby 154 | response.headers['X-Request-URL'] = request.url 155 | ``` 156 | 157 | If you want `response.url` to be reliable, you'll want to set this header. The 158 | day that you ditch this polyfill and use native fetch only, you can remove the 159 | header hack. 160 | 161 | ## Browser Support 162 | 163 | ![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/src/archive/internet-explorer_7-8/internet-explorer_7-8_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/src/safari/safari_48x48.png) 164 | --- | --- | --- | --- | --- | 165 | Latest ✔ | Latest ✔ | 8+ ✔ | Latest ✔ | 6.1+ ✔ | 166 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-ie8", 3 | "version": "1.3.1", 4 | "main": "fetch.js", 5 | "devDependencies": { 6 | "es6-promise": "2.1.0" 7 | }, 8 | "ignore": [ 9 | ".*", 10 | "*.md", 11 | "examples/", 12 | "Makefile", 13 | "package.json", 14 | "script/", 15 | "test/" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /fetch.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | 4 | // if __disableNativeFetch is set to true, the it will always polyfill fetch 5 | // with Ajax. 6 | if (!self.__disableNativeFetch && self.fetch) { 7 | return 8 | } 9 | 10 | function normalizeName(name) { 11 | if (typeof name !== 'string') { 12 | name = String(name) 13 | } 14 | if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { 15 | throw new TypeError('Invalid character in header field name') 16 | } 17 | return name.toLowerCase() 18 | } 19 | 20 | function normalizeValue(value) { 21 | if (typeof value !== 'string') { 22 | value = String(value) 23 | } 24 | return value 25 | } 26 | 27 | function Headers(headers) { 28 | this.map = {} 29 | 30 | if (headers instanceof Headers) { 31 | headers.forEach(function(value, name) { 32 | this.append(name, value) 33 | }, this) 34 | 35 | } else if (headers) { 36 | Object.getOwnPropertyNames(headers).forEach(function(name) { 37 | this.append(name, headers[name]) 38 | }, this) 39 | } 40 | } 41 | 42 | Headers.prototype.append = function(name, value) { 43 | name = normalizeName(name) 44 | value = normalizeValue(value) 45 | var list = this.map[name] 46 | if (!list) { 47 | list = [] 48 | this.map[name] = list 49 | } 50 | list.push(value) 51 | } 52 | 53 | Headers.prototype['delete'] = function(name) { 54 | delete this.map[normalizeName(name)] 55 | } 56 | 57 | Headers.prototype.get = function(name) { 58 | var values = this.map[normalizeName(name)] 59 | return values ? values[0] : null 60 | } 61 | 62 | Headers.prototype.getAll = function(name) { 63 | return this.map[normalizeName(name)] || [] 64 | } 65 | 66 | Headers.prototype.has = function(name) { 67 | return this.map.hasOwnProperty(normalizeName(name)) 68 | } 69 | 70 | Headers.prototype.set = function(name, value) { 71 | this.map[normalizeName(name)] = [normalizeValue(value)] 72 | } 73 | 74 | Headers.prototype.forEach = function(callback, thisArg) { 75 | Object.getOwnPropertyNames(this.map).forEach(function(name) { 76 | this.map[name].forEach(function(value) { 77 | callback.call(thisArg, value, name, this) 78 | }, this) 79 | }, this) 80 | } 81 | 82 | function consumed(body) { 83 | if (body.bodyUsed) { 84 | return Promise.reject(new TypeError('Already read')) 85 | } 86 | body.bodyUsed = true 87 | } 88 | 89 | function fileReaderReady(reader) { 90 | return new Promise(function(resolve, reject) { 91 | reader.onload = function() { 92 | resolve(reader.result) 93 | } 94 | reader.onerror = function() { 95 | reject(reader.error) 96 | } 97 | }) 98 | } 99 | 100 | function readBlobAsArrayBuffer(blob) { 101 | var reader = new FileReader() 102 | reader.readAsArrayBuffer(blob) 103 | return fileReaderReady(reader) 104 | } 105 | 106 | function readBlobAsText(blob, options) { 107 | var reader = new FileReader() 108 | var contentType = options.headers.map['content-type'] ? options.headers.map['content-type'].toString() : '' 109 | var regex = /charset\=[0-9a-zA-Z\-\_]*;?/ 110 | var _charset = blob.type.match(regex) || contentType.match(regex) 111 | var args = [blob] 112 | 113 | if(_charset) { 114 | args.push(_charset[0].replace(/^charset\=/, '').replace(/;$/, '')) 115 | } 116 | 117 | reader.readAsText.apply(reader, args) 118 | return fileReaderReady(reader) 119 | } 120 | 121 | var support = { 122 | blob: 'FileReader' in self && 'Blob' in self && (function() { 123 | try { 124 | new Blob(); 125 | return true 126 | } catch(e) { 127 | return false 128 | } 129 | })(), 130 | formData: 'FormData' in self, 131 | arrayBuffer: 'ArrayBuffer' in self 132 | } 133 | 134 | function Body() { 135 | this.bodyUsed = false 136 | 137 | 138 | this._initBody = function(body, options) { 139 | this._bodyInit = body 140 | if (typeof body === 'string') { 141 | this._bodyText = body 142 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { 143 | this._bodyBlob = body 144 | this._options = options 145 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { 146 | this._bodyFormData = body 147 | } else if (!body) { 148 | this._bodyText = '' 149 | } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) { 150 | // Only support ArrayBuffers for POST method. 151 | // Receiving ArrayBuffers happens via Blobs, instead. 152 | } else { 153 | throw new Error('unsupported BodyInit type') 154 | } 155 | } 156 | 157 | if (support.blob) { 158 | this.blob = function() { 159 | var rejected = consumed(this) 160 | if (rejected) { 161 | return rejected 162 | } 163 | 164 | if (this._bodyBlob) { 165 | return Promise.resolve(this._bodyBlob) 166 | } else if (this._bodyFormData) { 167 | throw new Error('could not read FormData body as blob') 168 | } else { 169 | return Promise.resolve(new Blob([this._bodyText])) 170 | } 171 | } 172 | 173 | this.arrayBuffer = function() { 174 | return this.blob().then(readBlobAsArrayBuffer) 175 | } 176 | 177 | this.text = function() { 178 | var rejected = consumed(this) 179 | if (rejected) { 180 | return rejected 181 | } 182 | 183 | if (this._bodyBlob) { 184 | return readBlobAsText(this._bodyBlob, this._options) 185 | } else if (this._bodyFormData) { 186 | throw new Error('could not read FormData body as text') 187 | } else { 188 | return Promise.resolve(this._bodyText) 189 | } 190 | } 191 | } else { 192 | this.text = function() { 193 | var rejected = consumed(this) 194 | return rejected ? rejected : Promise.resolve(this._bodyText) 195 | } 196 | } 197 | 198 | if (support.formData) { 199 | this.formData = function() { 200 | return this.text().then(decode) 201 | } 202 | } 203 | 204 | this.json = function() { 205 | return this.text().then(JSON.parse) 206 | } 207 | 208 | return this 209 | } 210 | 211 | // HTTP methods whose capitalization should be normalized 212 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] 213 | 214 | function normalizeMethod(method) { 215 | var upcased = method.toUpperCase() 216 | return (methods.indexOf(upcased) > -1) ? upcased : method 217 | } 218 | 219 | function Request(input, options) { 220 | options = options || {} 221 | var body = options.body 222 | if (Request.prototype.isPrototypeOf(input)) { 223 | if (input.bodyUsed) { 224 | throw new TypeError('Already read') 225 | } 226 | this.url = input.url 227 | this.credentials = input.credentials 228 | if (!options.headers) { 229 | this.headers = new Headers(input.headers) 230 | } 231 | this.method = input.method 232 | this.mode = input.mode 233 | if (!body) { 234 | body = input._bodyInit 235 | input.bodyUsed = true 236 | } 237 | } else { 238 | this.url = input 239 | } 240 | 241 | this.credentials = options.credentials || this.credentials || 'omit' 242 | if (options.headers || !this.headers) { 243 | this.headers = new Headers(options.headers) 244 | } 245 | this.method = normalizeMethod(options.method || this.method || 'GET') 246 | this.mode = options.mode || this.mode || null 247 | this.referrer = null 248 | 249 | if ((this.method === 'GET' || this.method === 'HEAD') && body) { 250 | throw new TypeError('Body not allowed for GET or HEAD requests') 251 | } 252 | this._initBody(body, options) 253 | } 254 | 255 | Request.prototype.clone = function() { 256 | return new Request(this) 257 | } 258 | 259 | function decode(body) { 260 | var form = new FormData() 261 | body.trim().split('&').forEach(function(bytes) { 262 | if (bytes) { 263 | var split = bytes.split('=') 264 | var name = split.shift().replace(/\+/g, ' ') 265 | var value = split.join('=').replace(/\+/g, ' ') 266 | form.append(decodeURIComponent(name), decodeURIComponent(value)) 267 | } 268 | }) 269 | return form 270 | } 271 | 272 | function headers(xhr) { 273 | var head = new Headers() 274 | var pairs = xhr.getAllResponseHeaders().trim().split('\n') 275 | pairs.forEach(function(header) { 276 | var split = header.trim().split(':') 277 | var key = split.shift().trim() 278 | var value = split.join(':').trim() 279 | head.append(key, value) 280 | }) 281 | return head 282 | } 283 | 284 | Body.call(Request.prototype) 285 | 286 | function Response(bodyInit, options) { 287 | if (!options) { 288 | options = {} 289 | } 290 | 291 | this._initBody(bodyInit, options) 292 | this.type = 'default' 293 | this.status = options.status 294 | this.ok = this.status >= 200 && this.status < 300 295 | this.statusText = options.statusText 296 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) 297 | this.url = options.url || '' 298 | } 299 | 300 | Body.call(Response.prototype) 301 | 302 | Response.prototype.clone = function() { 303 | return new Response(this._bodyInit, { 304 | status: this.status, 305 | statusText: this.statusText, 306 | headers: new Headers(this.headers), 307 | url: this.url 308 | }) 309 | } 310 | 311 | Response.error = function() { 312 | var response = new Response(null, {status: 0, statusText: ''}) 313 | response.type = 'error' 314 | return response 315 | } 316 | 317 | var redirectStatuses = [301, 302, 303, 307, 308] 318 | 319 | Response.redirect = function(url, status) { 320 | if (redirectStatuses.indexOf(status) === -1) { 321 | throw new RangeError('Invalid status code') 322 | } 323 | 324 | return new Response(null, {status: status, headers: {location: url}}) 325 | } 326 | 327 | self.Headers = Headers; 328 | self.Request = Request; 329 | self.Response = Response; 330 | 331 | self.fetch = function(input, init) { 332 | return new Promise(function(resolve, reject) { 333 | var request 334 | if (Request.prototype.isPrototypeOf(input) && !init) { 335 | request = input 336 | } else { 337 | request = new Request(input, init) 338 | } 339 | 340 | var xhr = new XMLHttpRequest() 341 | 342 | function responseURL() { 343 | if ('responseURL' in xhr) { 344 | return xhr.responseURL 345 | } 346 | 347 | // Avoid security warnings on getResponseHeader when not allowed by CORS 348 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { 349 | return xhr.getResponseHeader('X-Request-URL') 350 | } 351 | 352 | return; 353 | } 354 | 355 | var __onLoadHandled = false; 356 | 357 | function onload() { 358 | if (xhr.readyState !== 4) { 359 | return 360 | } 361 | var status = (xhr.status === 1223) ? 204 : xhr.status 362 | if (status < 100 || status > 599) { 363 | if (__onLoadHandled) { return; } else { __onLoadHandled = true; } 364 | reject(new TypeError('Network request failed')) 365 | return 366 | } 367 | var options = { 368 | status: status, 369 | statusText: xhr.statusText, 370 | headers: headers(xhr), 371 | url: responseURL() 372 | } 373 | var body = 'response' in xhr ? xhr.response : xhr.responseText; 374 | 375 | if (__onLoadHandled) { return; } else { __onLoadHandled = true; } 376 | resolve(new Response(body, options)) 377 | } 378 | xhr.onreadystatechange = onload; 379 | xhr.onload = onload; 380 | xhr.onerror = function() { 381 | if (__onLoadHandled) { return; } else { __onLoadHandled = true; } 382 | reject(new TypeError('Network request failed')) 383 | } 384 | 385 | xhr.open(request.method, request.url, true) 386 | 387 | // `withCredentials` should be setted after calling `.open` in IE10 388 | // http://stackoverflow.com/a/19667959/1219343 389 | try { 390 | if (request.credentials === 'include') { 391 | if ('withCredentials' in xhr) { 392 | xhr.withCredentials = true; 393 | } else { 394 | console && console.warn && console.warn('withCredentials is not supported, you can ignore this warning'); 395 | } 396 | } 397 | } catch (e) { 398 | console && console.warn && console.warn('set withCredentials error:' + e); 399 | } 400 | 401 | if ('responseType' in xhr && support.blob) { 402 | xhr.responseType = 'blob' 403 | } 404 | 405 | request.headers.forEach(function(value, name) { 406 | xhr.setRequestHeader(name, value) 407 | }) 408 | 409 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) 410 | }) 411 | } 412 | self.fetch.polyfill = true 413 | 414 | // Support CommonJS 415 | if (typeof module !== 'undefined' && module.exports) { 416 | module.exports = self.fetch; 417 | } 418 | })(typeof self !== 'undefined' ? self : this); 419 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-ie8", 3 | "version": "1.5.0", 4 | "main": "fetch.js", 5 | "repository": "camsong/fetch", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "browserify": "^9.0.8", 9 | "es6-promise": "^2.1.1", 10 | "isarray": "0.0.1", 11 | "jshint": "2.5.2", 12 | "mocha": "2.1.0" 13 | }, 14 | "files": [ 15 | "LICENSE", 16 | "fetch.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /script/phantomjs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | port=3900 6 | 7 | # Find next available port 8 | while lsof -i :$((++port)) >/dev/null; do true; done 9 | 10 | # Spin a test server in the background 11 | node ./script/server $port &>/dev/null & 12 | server_pid=$! 13 | trap "kill $server_pid" INT EXIT 14 | 15 | node ./node_modules/.bin/mocha-phantomjs -s localToRemoteUrlAccessEnabled=true -s webSecurityEnabled=false "http://localhost:$port/test/test.html" 16 | node ./node_modules/.bin/mocha-phantomjs -s localToRemoteUrlAccessEnabled=true -s webSecurityEnabled=false "http://localhost:$port/test/test-worker.html" 17 | -------------------------------------------------------------------------------- /script/saucelabs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | port=8080 6 | 7 | # Spin a test server in the background 8 | node ./script/server $port &>/dev/null & 9 | server_pid=$! 10 | trap "kill $server_pid" INT EXIT 11 | 12 | job=$(./script/saucelabs-start "http://localhost:$port/test/test.html") 13 | 14 | while true 15 | do 16 | result=$(echo "$job" | ./script/saucelabs-status) 17 | [[ $result == *"\"completed\": true"* ]] && break 18 | sleep 1 19 | echo -n "." 20 | done 21 | 22 | echo -n "" 23 | 24 | echo "$result" | ./script/saucelabs-result 25 | -------------------------------------------------------------------------------- /script/saucelabs-result: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | 5 | obj = JSON.parse(ARGF.read) 6 | 7 | test = obj['js tests'][0] 8 | 9 | warn test['url'] 10 | warn test['platform'] 11 | warn test['result'].inspect 12 | 13 | if test['result'] && (test['result']['passes'] + test['result']['pending']) == test['result']['tests'] 14 | exit 0 15 | else 16 | exit 1 17 | end 18 | -------------------------------------------------------------------------------- /script/saucelabs-start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | url="https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests" 6 | auth="$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" 7 | header="Content-Type: application/json" 8 | 9 | data=$(cat < 2 | 3 | 4 | 5 | Fetch Worker Tests 6 | 7 | 8 | 9 |
10 | 11 | 12 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fetch Tests 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | function readBlobAsText(blob) { 2 | if ('FileReader' in self) { 3 | return new Promise(function(resolve, reject) { 4 | var reader = new FileReader() 5 | reader.onload = function() { 6 | resolve(reader.result) 7 | } 8 | reader.onerror = function() { 9 | reject(reader.error) 10 | } 11 | reader.readAsText(blob) 12 | }) 13 | } else if ('FileReaderSync' in self) { 14 | return new FileReaderSync().readAsText(blob) 15 | } else { 16 | throw new ReferenceError('FileReader is not defined') 17 | } 18 | } 19 | 20 | function readBlobAsBytes(blob) { 21 | if ('FileReader' in self) { 22 | return new Promise(function(resolve, reject) { 23 | var reader = new FileReader() 24 | reader.onload = function() { 25 | var view = new Uint8Array(reader.result) 26 | resolve(Array.prototype.slice.call(view)) 27 | } 28 | reader.onerror = function() { 29 | reject(reader.error) 30 | } 31 | reader.readAsArrayBuffer(blob) 32 | }) 33 | } else if ('FileReaderSync' in self) { 34 | return new FileReaderSync().readAsArrayBuffer(blob) 35 | } else { 36 | throw new ReferenceError('FileReader is not defined') 37 | } 38 | } 39 | 40 | test('resolves promise on 500 error', function() { 41 | return fetch('/boom').then(function(response) { 42 | assert.equal(response.status, 500) 43 | assert.equal(response.ok, false) 44 | return response.text() 45 | }).then(function(body) { 46 | assert.equal(body, 'boom') 47 | }) 48 | }) 49 | 50 | test.skip('rejects promise for network error', function() { 51 | return fetch('/error').then(function(response) { 52 | assert(false, 'HTTP status ' + response.status + ' was treated as success') 53 | })['catch'](function(error) { 54 | assert(error instanceof TypeError, 'Rejected with Error') 55 | }) 56 | }) 57 | 58 | // https://fetch.spec.whatwg.org/#headers-class 59 | suite('Headers', function() { 60 | test('headers are case insensitive', function() { 61 | var headers = new Headers({'Accept': 'application/json'}) 62 | assert.equal(headers.get('ACCEPT'), 'application/json') 63 | assert.equal(headers.get('Accept'), 'application/json') 64 | assert.equal(headers.get('accept'), 'application/json') 65 | }) 66 | test('appends to existing', function() { 67 | var headers = new Headers({'Accept': 'application/json'}) 68 | assert.isFalse(headers.has('Content-Type')) 69 | headers.append('Content-Type', 'application/json') 70 | assert.isTrue(headers.has('Content-Type')) 71 | assert.equal(headers.get('Content-Type'), 'application/json') 72 | }) 73 | test('appends values to existing header name', function() { 74 | var headers = new Headers({'Accept': 'application/json'}) 75 | headers.append('Accept', 'text/plain') 76 | assert.equal(headers.getAll('Accept').length, 2) 77 | assert.equal(headers.getAll('Accept')[0], 'application/json') 78 | assert.equal(headers.getAll('Accept')[1], 'text/plain') 79 | }) 80 | test('sets header name and value', function() { 81 | var headers = new Headers() 82 | headers.set('Content-Type', 'application/json') 83 | assert.equal(headers.get('Content-Type'), 'application/json') 84 | }) 85 | test('returns null on no header found', function() { 86 | var headers = new Headers() 87 | assert.isNull(headers.get('Content-Type')) 88 | }) 89 | test('has headers that are set', function() { 90 | var headers = new Headers() 91 | headers.set('Content-Type', 'application/json') 92 | assert.isTrue(headers.has('Content-Type')) 93 | }) 94 | test('deletes headers', function() { 95 | var headers = new Headers() 96 | headers.set('Content-Type', 'application/json') 97 | assert.isTrue(headers.has('Content-Type')) 98 | headers['delete']('Content-Type') 99 | assert.isFalse(headers.has('Content-Type')) 100 | assert.isNull(headers.get('Content-Type')) 101 | }) 102 | test('returns list on getAll when header found', function() { 103 | var headers = new Headers({'Content-Type': 'application/json'}) 104 | assert.isArray(headers.getAll('Content-Type')) 105 | assert.equal(headers.getAll('Content-Type').length, 1) 106 | assert.equal(headers.getAll('Content-Type')[0], 'application/json') 107 | }) 108 | test('returns empty list on getAll when no header found', function() { 109 | var headers = new Headers() 110 | assert.isArray(headers.getAll('Content-Type')) 111 | assert.equal(headers.getAll('Content-Type').length, 0) 112 | }) 113 | test('converts field name to string on set and get', function() { 114 | var headers = new Headers() 115 | headers.set(1, 'application/json') 116 | assert.equal(headers.get(1), 'application/json') 117 | }) 118 | test('converts field value to string on set and get', function() { 119 | var headers = new Headers() 120 | headers.set('Content-Type', 1) 121 | assert.equal(headers.get('Content-Type'), '1') 122 | }) 123 | test('throws TypeError on invalid character in field name', function() { 124 | assert.throws(function() { new Headers({'': ['application/json']}) }, TypeError) 125 | assert.throws(function() { new Headers({'Accept:': ['application/json']}) }, TypeError) 126 | assert.throws(function() { 127 | var headers = new Headers(); 128 | headers.set({field: 'value'}, 'application/json'); 129 | }, TypeError) 130 | }) 131 | }) 132 | 133 | // https://fetch.spec.whatwg.org/#request-class 134 | suite('Request', function() { 135 | test('sends request headers', function() { 136 | return fetch('/request', { 137 | headers: { 138 | 'Accept': 'application/json', 139 | 'X-Test': '42' 140 | } 141 | }).then(function(response) { 142 | return response.json() 143 | }).then(function(json) { 144 | assert.equal(json.headers['accept'], 'application/json') 145 | assert.equal(json.headers['x-test'], '42') 146 | }) 147 | }) 148 | 149 | test('fetch request', function() { 150 | var request = new Request('/request', { 151 | headers: { 152 | 'Accept': 'application/json', 153 | 'X-Test': '42' 154 | } 155 | }) 156 | 157 | return fetch(request).then(function(response) { 158 | return response.json() 159 | }).then(function(json) { 160 | assert.equal(json.headers['accept'], 'application/json') 161 | assert.equal(json.headers['x-test'], '42') 162 | }) 163 | }) 164 | 165 | test('construct with url', function() { 166 | var request = new Request('https://fetch.spec.whatwg.org/') 167 | assert.equal(request.url, 'https://fetch.spec.whatwg.org/') 168 | }) 169 | 170 | // https://fetch.spec.whatwg.org/#concept-bodyinit-extract 171 | suite('BodyInit extract', function() { 172 | ;(Request.prototype.blob ? suite : suite.skip)('type Blob', function() { 173 | test('consume as blob', function() { 174 | var request = new Request(null, {method: 'POST', body: new Blob(['hello'])}) 175 | return request.blob().then(readBlobAsText).then(function(text) { 176 | assert.equal(text, 'hello') 177 | }) 178 | }) 179 | 180 | test('consume as text', function() { 181 | var request = new Request(null, {method: 'POST', body: new Blob(['hello'])}) 182 | return request.text().then(function(text) { 183 | assert.equal(text, 'hello') 184 | }) 185 | }) 186 | }) 187 | 188 | suite('type USVString', function() { 189 | test('consume as text', function() { 190 | var request = new Request(null, {method: 'POST', body: 'hello'}) 191 | return request.text().then(function(text) { 192 | assert.equal(text, 'hello') 193 | }) 194 | }) 195 | 196 | ;(Request.prototype.blob ? test : test.skip)('consume as blob', function() { 197 | var request = new Request(null, {method: 'POST', body: 'hello'}) 198 | return request.blob().then(readBlobAsText).then(function(text) { 199 | assert.equal(text, 'hello') 200 | }) 201 | }) 202 | }) 203 | }) 204 | }) 205 | 206 | // https://fetch.spec.whatwg.org/#response-class 207 | suite('Response', function() { 208 | // https://fetch.spec.whatwg.org/#concept-bodyinit-extract 209 | suite('BodyInit extract', function() { 210 | ;(Response.prototype.blob ? suite : suite.skip)('type Blob', function() { 211 | test('consume as blob', function() { 212 | var response = new Response(new Blob(['hello'])) 213 | return response.blob().then(readBlobAsText).then(function(text) { 214 | assert.equal(text, 'hello') 215 | }) 216 | }) 217 | 218 | test('consume as text', function() { 219 | var response = new Response(new Blob(['hello'])) 220 | return response.text().then(function(text) { 221 | assert.equal(text, 'hello') 222 | }) 223 | }) 224 | }) 225 | 226 | suite('type USVString', function() { 227 | test('consume as text', function() { 228 | var response = new Response('hello') 229 | return response.text().then(function(text) { 230 | assert.equal(text, 'hello') 231 | }) 232 | }) 233 | 234 | ;(Response.prototype.blob ? test : test.skip)('consume as blob', function() { 235 | var response = new Response('hello') 236 | return response.blob().then(readBlobAsText).then(function(text) { 237 | assert.equal(text, 'hello') 238 | }) 239 | }) 240 | }) 241 | }) 242 | 243 | test('populates response body', function() { 244 | return fetch('/hello').then(function(response) { 245 | assert.equal(response.status, 200) 246 | assert.equal(response.ok, true) 247 | return response.text() 248 | }).then(function(body) { 249 | assert.equal(body, 'hi') 250 | }) 251 | }) 252 | 253 | test('parses response headers', function() { 254 | return fetch('/headers?' + new Date().getTime()).then(function(response) { 255 | assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT') 256 | assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8') 257 | }) 258 | }) 259 | 260 | test('creates Headers object from raw headers', function() { 261 | var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}}); 262 | assert.equal(r.headers instanceof Headers, true); 263 | return r.json().then(function(json){ 264 | assert(json.foo, 'bar'); 265 | return json; 266 | }) 267 | }) 268 | }) 269 | 270 | // https://fetch.spec.whatwg.org/#body-mixin 271 | suite('Body mixin', function() { 272 | ;(Response.prototype.arrayBuffer ? suite : suite.skip)('arrayBuffer', function() { 273 | test('resolves arrayBuffer promise', function() { 274 | return fetch('/hello').then(function(response) { 275 | return response.arrayBuffer() 276 | }).then(function(buf) { 277 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 278 | assert.equal(buf.byteLength, 2) 279 | }) 280 | }) 281 | 282 | test('arrayBuffer handles binary data', function() { 283 | return fetch('/binary').then(function(response) { 284 | return response.arrayBuffer() 285 | }).then(function(buf) { 286 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 287 | assert.equal(buf.byteLength, 256, 'buf.byteLength is correct') 288 | var view = new Uint8Array(buf) 289 | for (var i = 0; i < 256; i++) { 290 | assert.equal(view[i], i) 291 | } 292 | }) 293 | }) 294 | 295 | test('arrayBuffer handles utf-8 data', function() { 296 | return fetch('/hello/utf8').then(function(response) { 297 | return response.arrayBuffer() 298 | }).then(function(buf) { 299 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 300 | assert.equal(buf.byteLength, 5, 'buf.byteLength is correct') 301 | var octets = Array.prototype.slice.call(new Uint8Array(buf)) 302 | assert.deepEqual(octets, [104, 101, 108, 108, 111]) 303 | }) 304 | }) 305 | 306 | test('arrayBuffer handles utf-16le data', function() { 307 | return fetch('/hello/utf16le').then(function(response) { 308 | return response.arrayBuffer() 309 | }).then(function(buf) { 310 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 311 | assert.equal(buf.byteLength, 10, 'buf.byteLength is correct') 312 | var octets = Array.prototype.slice.call(new Uint8Array(buf)) 313 | assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0]) 314 | }) 315 | }) 316 | 317 | test('rejects arrayBuffer promise after body is consumed', function() { 318 | return fetch('/hello').then(function(response) { 319 | assert(response.arrayBuffer, 'Body does not implement arrayBuffer') 320 | assert.equal(response.bodyUsed, false) 321 | response.blob() 322 | assert.equal(response.bodyUsed, true) 323 | return response.arrayBuffer() 324 | })['catch'](function(error) { 325 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 326 | }) 327 | }) 328 | }) 329 | 330 | ;(Response.prototype.blob ? suite : suite.skip)('blob', function() { 331 | test('resolves blob promise', function() { 332 | return fetch('/hello').then(function(response) { 333 | return response.blob() 334 | }).then(function(blob) { 335 | assert(blob instanceof Blob, 'blob is a Blob instance') 336 | assert.equal(blob.size, 2) 337 | }) 338 | }) 339 | 340 | test('blob handles binary data', function() { 341 | return fetch('/binary').then(function(response) { 342 | return response.blob() 343 | }).then(function(blob) { 344 | assert(blob instanceof Blob, 'blob is a Blob instance') 345 | assert.equal(blob.size, 256, 'blob.size is correct') 346 | }) 347 | }) 348 | 349 | test('blob handles utf-8 data', function() { 350 | return fetch('/hello/utf8').then(function(response) { 351 | return response.blob() 352 | }).then(readBlobAsBytes).then(function(octets) { 353 | assert.equal(octets.length, 5, 'blob.size is correct') 354 | assert.deepEqual(octets, [104, 101, 108, 108, 111]) 355 | }) 356 | }) 357 | 358 | test('blob handles utf-16le data', function() { 359 | return fetch('/hello/utf16le').then(function(response) { 360 | return response.blob() 361 | }).then(readBlobAsBytes).then(function(octets) { 362 | assert.equal(octets.length, 10, 'blob.size is correct') 363 | assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0]) 364 | }) 365 | }) 366 | 367 | test('rejects blob promise after body is consumed', function() { 368 | return fetch('/hello').then(function(response) { 369 | assert(response.blob, 'Body does not implement blob') 370 | assert.equal(response.bodyUsed, false) 371 | response.text() 372 | assert.equal(response.bodyUsed, true) 373 | return response.blob() 374 | })['catch'](function(error) { 375 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 376 | }) 377 | }) 378 | }) 379 | 380 | ;(Response.prototype.formData ? suite : suite.skip)('formData', function() { 381 | test('post sets content-type header', function() { 382 | return fetch('/request', { 383 | method: 'post', 384 | body: new FormData() 385 | }).then(function(response) { 386 | return response.json() 387 | }).then(function(json) { 388 | assert.equal(json.method, 'POST') 389 | assert(/^multipart\/form-data;/.test(json.headers['content-type'])) 390 | }) 391 | }) 392 | 393 | test('rejects formData promise after body is consumed', function() { 394 | return fetch('/json').then(function(response) { 395 | assert(response.formData, 'Body does not implement formData') 396 | response.formData() 397 | return response.formData() 398 | })['catch'](function(error) { 399 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 400 | }) 401 | }) 402 | 403 | test('parses form encoded response', function() { 404 | return fetch('/form').then(function(response) { 405 | return response.formData() 406 | }).then(function(form) { 407 | assert(form instanceof FormData, 'Parsed a FormData object') 408 | }) 409 | }) 410 | }) 411 | 412 | suite('json', function() { 413 | test('parses json response', function() { 414 | return fetch('/json').then(function(response) { 415 | return response.json() 416 | }).then(function(json) { 417 | assert.equal(json.name, 'Hubot') 418 | assert.equal(json.login, 'hubot') 419 | }) 420 | }) 421 | 422 | test('rejects json promise after body is consumed', function() { 423 | return fetch('/json').then(function(response) { 424 | assert(response.json, 'Body does not implement json') 425 | assert.equal(response.bodyUsed, false) 426 | response.text() 427 | assert.equal(response.bodyUsed, true) 428 | return response.json() 429 | })['catch'](function(error) { 430 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 431 | }) 432 | }) 433 | 434 | test('handles json parse error', function() { 435 | return fetch('/json-error').then(function(response) { 436 | return response.json() 437 | })['catch'](function(error) { 438 | assert(error instanceof Error, 'JSON exception is an Error instance') 439 | assert(error.message, 'JSON exception has an error message') 440 | }) 441 | }) 442 | }) 443 | 444 | suite('text', function() { 445 | test('handles 204 No Content response', function() { 446 | return fetch('/empty').then(function(response) { 447 | assert.equal(response.status, 204) 448 | return response.text() 449 | }).then(function(body) { 450 | assert.equal(body, '') 451 | }) 452 | }) 453 | 454 | test('resolves text promise', function() { 455 | return fetch('/hello').then(function(response) { 456 | return response.text() 457 | }).then(function(text) { 458 | assert.equal(text, 'hi') 459 | }) 460 | }) 461 | 462 | test('rejects text promise after body is consumed', function() { 463 | return fetch('/hello').then(function(response) { 464 | assert(response.text, 'Body does not implement text') 465 | assert.equal(response.bodyUsed, false) 466 | response.text() 467 | assert.equal(response.bodyUsed, true) 468 | return response.text() 469 | })['catch'](function(error) { 470 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 471 | }) 472 | }) 473 | }) 474 | }) 475 | 476 | // https://fetch.spec.whatwg.org/#methods 477 | suite('Methods', function() { 478 | test('supports HTTP GET', function() { 479 | return fetch('/request', { 480 | method: 'get', 481 | }).then(function(response) { 482 | return response.json() 483 | }).then(function(request) { 484 | assert.equal(request.method, 'GET') 485 | assert.equal(request.data, '') 486 | }) 487 | }) 488 | 489 | // TODO: Waiting to verify behavior 490 | test.skip('GET with body throws TypeError', function() { 491 | assert.throws(function() { 492 | new Request('', { 493 | method: 'get', 494 | body: 'invalid' 495 | }) 496 | }, TypeError) 497 | }) 498 | 499 | test.skip('HEAD with body throws TypeError', function() { 500 | assert.throws(function() { 501 | new Request('', { 502 | method: 'head', 503 | body: 'invalid' 504 | }) 505 | }, TypeError) 506 | }) 507 | 508 | test('supports HTTP POST', function() { 509 | return fetch('/request', { 510 | method: 'post', 511 | body: 'name=Hubot' 512 | }).then(function(response) { 513 | return response.json() 514 | }).then(function(request) { 515 | assert.equal(request.method, 'POST') 516 | assert.equal(request.data, 'name=Hubot') 517 | }) 518 | }) 519 | 520 | test('supports HTTP PUT', function() { 521 | return fetch('/request', { 522 | method: 'put', 523 | body: 'name=Hubot' 524 | }).then(function(response) { 525 | return response.json() 526 | }).then(function(request) { 527 | assert.equal(request.method, 'PUT') 528 | assert.equal(request.data, 'name=Hubot') 529 | }) 530 | }) 531 | 532 | var patchSupported = !/PhantomJS/.test(navigator.userAgent) 533 | 534 | ;(patchSupported ? test : test.skip)('supports HTTP PATCH', function() { 535 | return fetch('/request', { 536 | method: 'PATCH', 537 | body: 'name=Hubot' 538 | }).then(function(response) { 539 | return response.json() 540 | }).then(function(request) { 541 | assert.equal(request.method, 'PATCH') 542 | assert.equal(request.data, 'name=Hubot') 543 | }) 544 | }) 545 | 546 | test('supports HTTP DELETE', function() { 547 | return fetch('/request', { 548 | method: 'delete', 549 | }).then(function(response) { 550 | return response.json() 551 | }).then(function(request) { 552 | assert.equal(request.method, 'DELETE') 553 | assert.equal(request.data, '') 554 | }) 555 | }) 556 | }) 557 | 558 | // https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 559 | suite('Atomic HTTP redirect handling', function() { 560 | test('handles 301 redirect response', function() { 561 | return fetch('/redirect/301').then(function(response) { 562 | assert.equal(response.status, 200) 563 | assert.equal(response.ok, true) 564 | assert.match(response.url, /\/hello/) 565 | return response.text() 566 | }).then(function(body) { 567 | assert.equal(body, 'hi') 568 | }) 569 | }) 570 | 571 | test('handles 302 redirect response', function() { 572 | return fetch('/redirect/302').then(function(response) { 573 | assert.equal(response.status, 200) 574 | assert.equal(response.ok, true) 575 | assert.match(response.url, /\/hello/) 576 | return response.text() 577 | }).then(function(body) { 578 | assert.equal(body, 'hi') 579 | }) 580 | }) 581 | 582 | test('handles 303 redirect response', function() { 583 | return fetch('/redirect/303').then(function(response) { 584 | assert.equal(response.status, 200) 585 | assert.equal(response.ok, true) 586 | assert.match(response.url, /\/hello/) 587 | return response.text() 588 | }).then(function(body) { 589 | assert.equal(body, 'hi') 590 | }) 591 | }) 592 | 593 | test('handles 307 redirect response', function() { 594 | return fetch('/redirect/307').then(function(response) { 595 | assert.equal(response.status, 200) 596 | assert.equal(response.ok, true) 597 | assert.match(response.url, /\/hello/) 598 | return response.text() 599 | }).then(function(body) { 600 | assert.equal(body, 'hi') 601 | }) 602 | }) 603 | 604 | var permanentRedirectSupported = !/PhantomJS|Trident/.test(navigator.userAgent) 605 | 606 | ;(permanentRedirectSupported ? test : test.skip)('handles 308 redirect response', function() { 607 | return fetch('/redirect/308').then(function(response) { 608 | assert.equal(response.status, 200) 609 | assert.equal(response.ok, true) 610 | assert.match(response.url, /\/hello/) 611 | return response.text() 612 | }).then(function(body) { 613 | assert.equal(body, 'hi') 614 | }) 615 | }) 616 | }) 617 | 618 | // https://fetch.spec.whatwg.org/#concept-request-credentials-mode 619 | suite('credentials mode', function() { 620 | var omitSupported = !self.fetch.polyfill 621 | 622 | setup(function() { 623 | return fetch('/cookie?name=foo&value=reset', {credentials: 'same-origin'}); 624 | }) 625 | 626 | ;(omitSupported ? suite : suite.skip)('omit', function() { 627 | test('request credentials defaults to omit', function() { 628 | var request = new Request('') 629 | assert.equal(request.credentials, 'omit') 630 | }) 631 | 632 | test('does not accept cookies with implicit omit credentials', function() { 633 | return fetch('/cookie?name=foo&value=bar').then(function() { 634 | return fetch('/cookie?name=foo', {credentials: 'same-origin'}); 635 | }).then(function(response) { 636 | return response.text() 637 | }).then(function(data) { 638 | assert.equal(data, 'reset') 639 | }) 640 | }) 641 | 642 | test('does not accept cookies with omit credentials', function() { 643 | return fetch('/cookie?name=foo&value=bar', {credentials: 'omit'}).then(function() { 644 | return fetch('/cookie?name=foo', {credentials: 'same-origin'}); 645 | }).then(function(response) { 646 | return response.text() 647 | }).then(function(data) { 648 | assert.equal(data, 'reset') 649 | }) 650 | }) 651 | 652 | test('does not send cookies with implicit omit credentials', function() { 653 | return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'}).then(function() { 654 | return fetch('/cookie?name=foo'); 655 | }).then(function(response) { 656 | return response.text() 657 | }).then(function(data) { 658 | assert.equal(data, '') 659 | }) 660 | }) 661 | 662 | test('does not send cookies with omit credentials', function() { 663 | return fetch('/cookie?name=foo&value=bar').then(function() { 664 | return fetch('/cookie?name=foo', {credentials: 'omit'}) 665 | }).then(function(response) { 666 | return response.text() 667 | }).then(function(data) { 668 | assert.equal(data, '') 669 | }) 670 | }) 671 | }) 672 | 673 | suite('same-origin', function() { 674 | test('request credentials uses inits member', function() { 675 | var request = new Request('', {credentials: 'same-origin'}) 676 | assert.equal(request.credentials, 'same-origin') 677 | }) 678 | 679 | test('send cookies with same-origin credentials', function() { 680 | return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'}).then(function() { 681 | return fetch('/cookie?name=foo', {credentials: 'same-origin'}) 682 | }).then(function(response) { 683 | return response.text() 684 | }).then(function(data) { 685 | assert.equal(data, 'bar') 686 | }) 687 | }) 688 | }) 689 | 690 | suite('include', function() { 691 | test('send cookies with include credentials', function() { 692 | return fetch('/cookie?name=foo&value=bar', {credentials: 'include'}).then(function() { 693 | return fetch('/cookie?name=foo', {credentials: 'include'}) 694 | }).then(function(response) { 695 | return response.text() 696 | }).then(function(data) { 697 | assert.equal(data, 'bar') 698 | }) 699 | }) 700 | }) 701 | }) 702 | -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../node_modules/chai/chai.js') 2 | importScripts('../node_modules/mocha/mocha.js') 3 | 4 | mocha.setup('tdd') 5 | self.assert = chai.assert 6 | 7 | importScripts('../bower_components/es6-promise/promise.js') 8 | importScripts('../fetch.js') 9 | 10 | importScripts('test.js') 11 | 12 | function title(test) { 13 | return test.fullTitle().replace(/#/g, ''); 14 | } 15 | 16 | function reporter(runner) { 17 | runner.on('pending', function(test){ 18 | self.postMessage({name: 'pending', title: title(test)}); 19 | }); 20 | 21 | runner.on('pass', function(test){ 22 | self.postMessage({name: 'pass', title: title(test)}); 23 | }); 24 | 25 | runner.on('fail', function(test, err){ 26 | self.postMessage({ 27 | name: 'fail', 28 | title: title(test), 29 | message: err.message, 30 | stack: err.stack 31 | }); 32 | }); 33 | 34 | runner.on('end', function(){ 35 | self.postMessage({name: 'end'}); 36 | }); 37 | } 38 | 39 | mocha.reporter(reporter).run() 40 | --------------------------------------------------------------------------------