├── .gitignore ├── .jshintrc ├── .senv ├── .travis.yml ├── 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 | -------------------------------------------------------------------------------- /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 | **This module is out of maintanance, use [fetch-ie8](https://www.npmjs.com/package/fetch-ie8) instead. or you can try [promisingagent](https://www.npmjs.com/package/promisingagent) if you want to use promise api for ajax and don't need ServiceWorker** 6 | 7 | The global `fetch` function is an easier way to make web requests and handle 8 | responses than using an XMLHttpRequest. This polyfill is written as closely as 9 | possible to the standard Fetch specification at https://fetch.spec.whatwg.org. 10 | 11 | ## Installation 12 | 13 | Available on [Bower](http://bower.io) as **fetch-polyfill**. 14 | 15 | ```sh 16 | $ bower install fetch-polyfill 17 | ``` 18 | 19 | You'll also need a Promise polyfill for older browsers. 20 | 21 | ```sh 22 | $ bower install es6-promise 23 | ``` 24 | 25 | This can also be installed with `npm`. 26 | 27 | ```sh 28 | $ npm install fetch-polyfill --save 29 | ``` 30 | 31 | (For a node.js implementation, try [node-fetch](https://github.com/bitinn/node-fetch)) 32 | 33 | ## Usage 34 | 35 | The `fetch` function supports any HTTP method. We'll focus on GET and POST 36 | example requests. 37 | 38 | ### HTML 39 | 40 | ```javascript 41 | fetch('/users.html') 42 | .then(function(response) { 43 | return response.text() 44 | }).then(function(body) { 45 | document.body.innerHTML = body 46 | }) 47 | ``` 48 | 49 | ### JSON 50 | 51 | ```javascript 52 | fetch('/users.json') 53 | .then(function(response) { 54 | return response.json() 55 | }).then(function(json) { 56 | console.log('parsed json', json) 57 | }).catch(function(ex) { 58 | console.log('parsing failed', ex) 59 | }) 60 | ``` 61 | 62 | ### Response metadata 63 | 64 | ```javascript 65 | fetch('/users.json').then(function(response) { 66 | console.log(response.headers.get('Content-Type')) 67 | console.log(response.headers.get('Date')) 68 | console.log(response.status) 69 | console.log(response.statusText) 70 | }) 71 | ``` 72 | 73 | ### Post form 74 | 75 | ```javascript 76 | var form = document.querySelector('form') 77 | 78 | fetch('/query', { 79 | method: 'post', 80 | body: new FormData(form) 81 | }) 82 | ``` 83 | 84 | ### Post JSON 85 | 86 | ```javascript 87 | fetch('/users', { 88 | method: 'post', 89 | headers: { 90 | 'Accept': 'application/json', 91 | 'Content-Type': 'application/json' 92 | }, 93 | body: JSON.stringify({ 94 | name: 'Hubot', 95 | login: 'hubot', 96 | }) 97 | }) 98 | ``` 99 | 100 | ### File upload 101 | 102 | ```javascript 103 | var input = document.querySelector('input[type="file"]') 104 | 105 | var form = new FormData() 106 | form.append('file', input.files[0]) 107 | form.append('user', 'hubot') 108 | 109 | fetch('/avatars', { 110 | method: 'post', 111 | body: form 112 | }) 113 | ``` 114 | 115 | ### Success and error handlers 116 | 117 | This causes `fetch` to behave like jQuery's `$.ajax` by rejecting the `Promise` 118 | on HTTP failure status codes like 404, 500, etc. The response `Promise` is 119 | resolved only on successful, 200 level, status codes. 120 | 121 | ```javascript 122 | function status(response) { 123 | if (response.status >= 200 && response.status < 300) { 124 | return response 125 | } 126 | throw new Error(response.statusText) 127 | } 128 | 129 | function json(response) { 130 | return response.json() 131 | } 132 | 133 | fetch('/users') 134 | .then(status) 135 | .then(json) 136 | .then(function(json) { 137 | console.log('request succeeded with json response', json) 138 | }).catch(function(error) { 139 | console.log('request failed', error) 140 | }) 141 | ``` 142 | 143 | ### Response URL caveat 144 | 145 | The `Response` object has a URL attribute for the final responded resource. 146 | Usually this is the same as the `Request` url, but in the case of a redirect, 147 | its all transparent. Newer versions of XHR include a `responseURL` attribute 148 | that returns this value. But not every browser supports this. The compromise 149 | requires setting a special server side header to tell the browser what URL it 150 | just requested (yeah, I know browsers). 151 | 152 | ``` ruby 153 | response.headers['X-Request-URL'] = request.url 154 | ``` 155 | 156 | If you want `response.url` to be reliable, you'll want to set this header. The 157 | day that you ditch this polyfill and use native fetch only, you can remove the 158 | header hack. 159 | 160 | ## Browser Support 161 | 162 | ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) 163 | --- | --- | --- | --- | --- | 164 | Latest ✔ | Latest ✔ | 8+ ✔ | Latest ✔ | 6.1+ ✔ | 165 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-polyfill", 3 | "version": "0.8.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() { 2 | 'use strict'; 3 | 4 | if (self.fetch) { 5 | return 6 | } 7 | 8 | function normalizeName(name) { 9 | if (typeof name !== 'string') { 10 | name = name.toString(); 11 | } 12 | if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { 13 | throw new TypeError('Invalid character in header field name') 14 | } 15 | return name.toLowerCase() 16 | } 17 | 18 | function normalizeValue(value) { 19 | if (typeof value !== 'string') { 20 | value = value.toString(); 21 | } 22 | return value 23 | } 24 | 25 | function Headers(headers) { 26 | this.map = {} 27 | 28 | var self = this 29 | if (headers instanceof Headers) { 30 | headers.forEach(function(name, values) { 31 | values.forEach(function(value) { 32 | self.append(name, value) 33 | }) 34 | }) 35 | 36 | } else if (headers) { 37 | Object.getOwnPropertyNames(headers).forEach(function(name) { 38 | self.append(name, headers[name]) 39 | }) 40 | } 41 | } 42 | 43 | Headers.prototype.append = function(name, value) { 44 | name = normalizeName(name) 45 | value = normalizeValue(value) 46 | var list = this.map[name] 47 | if (!list) { 48 | list = [] 49 | this.map[name] = list 50 | } 51 | list.push(value) 52 | } 53 | 54 | Headers.prototype['delete'] = function(name) { 55 | delete this.map[normalizeName(name)] 56 | } 57 | 58 | Headers.prototype.get = function(name) { 59 | var values = this.map[normalizeName(name)] 60 | return values ? values[0] : null 61 | } 62 | 63 | Headers.prototype.getAll = function(name) { 64 | return this.map[normalizeName(name)] || [] 65 | } 66 | 67 | Headers.prototype.has = function(name) { 68 | return this.map.hasOwnProperty(normalizeName(name)) 69 | } 70 | 71 | Headers.prototype.set = function(name, value) { 72 | this.map[normalizeName(name)] = [normalizeValue(value)] 73 | } 74 | 75 | // Instead of iterable for now. 76 | Headers.prototype.forEach = function(callback) { 77 | var self = this 78 | Object.getOwnPropertyNames(this.map).forEach(function(name) { 79 | callback(name, self.map[name]) 80 | }) 81 | } 82 | 83 | function consumed(body) { 84 | if (body.bodyUsed) { 85 | return fetch.Promise.reject(new TypeError('Already read')) 86 | } 87 | body.bodyUsed = true 88 | } 89 | 90 | function fileReaderReady(reader) { 91 | return new fetch.Promise(function(resolve, reject) { 92 | reader.onload = function() { 93 | resolve(reader.result) 94 | } 95 | reader.onerror = function() { 96 | reject(reader.error) 97 | } 98 | }) 99 | } 100 | 101 | function readBlobAsArrayBuffer(blob) { 102 | var reader = new FileReader() 103 | reader.readAsArrayBuffer(blob) 104 | return fileReaderReady(reader) 105 | } 106 | 107 | function readBlobAsText(blob) { 108 | var reader = new FileReader() 109 | reader.readAsText(blob) 110 | return fileReaderReady(reader) 111 | } 112 | 113 | var support = { 114 | blob: 'FileReader' in self && 'Blob' in self && (function() { 115 | try { 116 | new Blob(); 117 | return true 118 | } catch(e) { 119 | return false 120 | } 121 | })(), 122 | formData: 'FormData' in self 123 | } 124 | 125 | function Body() { 126 | this.bodyUsed = false 127 | 128 | 129 | this._initBody = function(body) { 130 | this._bodyInit = body 131 | if (typeof body === 'string') { 132 | this._bodyText = body 133 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { 134 | this._bodyBlob = body 135 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { 136 | this._bodyFormData = body 137 | } else if (!body) { 138 | this._bodyText = '' 139 | } else { 140 | throw new Error('unsupported BodyInit type') 141 | } 142 | } 143 | 144 | if (support.blob) { 145 | this.blob = function() { 146 | var rejected = consumed(this) 147 | if (rejected) { 148 | return rejected 149 | } 150 | 151 | if (this._bodyBlob) { 152 | return fetch.Promise.resolve(this._bodyBlob) 153 | } else if (this._bodyFormData) { 154 | throw new Error('could not read FormData body as blob') 155 | } else { 156 | return fetch.Promise.resolve(new Blob([this._bodyText])) 157 | } 158 | } 159 | 160 | this.arrayBuffer = function() { 161 | return this.blob().then(readBlobAsArrayBuffer) 162 | } 163 | 164 | this.text = function() { 165 | var rejected = consumed(this) 166 | if (rejected) { 167 | return rejected 168 | } 169 | 170 | if (this._bodyBlob) { 171 | return readBlobAsText(this._bodyBlob) 172 | } else if (this._bodyFormData) { 173 | throw new Error('could not read FormData body as text') 174 | } else { 175 | return fetch.Promise.resolve(this._bodyText) 176 | } 177 | } 178 | } else { 179 | this.text = function() { 180 | var rejected = consumed(this) 181 | return rejected ? rejected : fetch.Promise.resolve(this._bodyText) 182 | } 183 | } 184 | 185 | if (support.formData) { 186 | this.formData = function() { 187 | return this.text().then(decode) 188 | } 189 | } 190 | 191 | this.json = function() { 192 | return this.text().then(function (text) { 193 | return JSON.parse(text); 194 | }); 195 | } 196 | 197 | return this 198 | } 199 | 200 | // HTTP methods whose capitalization should be normalized 201 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] 202 | 203 | function normalizeMethod(method) { 204 | var upcased = method.toUpperCase() 205 | return (methods.indexOf(upcased) > -1) ? upcased : method 206 | } 207 | 208 | function Request(url, options) { 209 | options = options || {} 210 | this.url = url 211 | 212 | this.credentials = options.credentials || 'omit' 213 | this.headers = new Headers(options.headers) 214 | this.method = normalizeMethod(options.method || 'GET') 215 | this.mode = options.mode || null 216 | this.referrer = null 217 | 218 | if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { 219 | throw new TypeError('Body not allowed for GET or HEAD requests') 220 | } 221 | this._initBody(options.body) 222 | } 223 | 224 | function decode(body) { 225 | var form = new FormData() 226 | body.trim().split('&').forEach(function(bytes) { 227 | if (bytes) { 228 | var split = bytes.split('=') 229 | var name = split.shift().replace(/\+/g, ' ') 230 | var value = split.join('=').replace(/\+/g, ' ') 231 | form.append(decodeURIComponent(name), decodeURIComponent(value)) 232 | } 233 | }) 234 | return form 235 | } 236 | 237 | function headers(xhr) { 238 | var head = new Headers() 239 | var pairs = xhr.getAllResponseHeaders().trim().split('\n') 240 | pairs.forEach(function(header) { 241 | var split = header.trim().split(':') 242 | var key = split.shift().trim() 243 | var value = split.join(':').trim() 244 | head.append(key, value) 245 | }) 246 | return head 247 | } 248 | 249 | var noXhrPatch = 250 | typeof window !== 'undefined' && !!window.ActiveXObject && 251 | !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); 252 | 253 | function getXhr() { 254 | // from backbone.js 1.1.2 255 | // https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L1181 256 | if (noXhrPatch && !(/^(get|post|head|put|delete|options)$/i.test(this.method))) { 257 | this.usingActiveXhr = true; 258 | return new ActiveXObject("Microsoft.XMLHTTP"); 259 | } 260 | return new XMLHttpRequest(); 261 | } 262 | 263 | Body.call(Request.prototype) 264 | 265 | function Response(bodyInit, options) { 266 | if (!options) { 267 | options = {} 268 | } 269 | 270 | this._initBody(bodyInit) 271 | this.type = 'default' 272 | this.url = null 273 | this.status = options.status 274 | this.ok = this.status >= 200 && this.status < 300 275 | this.statusText = options.statusText 276 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) 277 | this.url = options.url || '' 278 | } 279 | 280 | Body.call(Response.prototype) 281 | 282 | self.Headers = Headers; 283 | self.Request = Request; 284 | self.Response = Response; 285 | 286 | self.fetch = function(input, init) { 287 | // TODO: Request constructor should accept input, init 288 | var request 289 | if (Request.prototype.isPrototypeOf(input) && !init) { 290 | request = input 291 | } else { 292 | request = new Request(input, init) 293 | } 294 | 295 | return new fetch.Promise(function(resolve, reject) { 296 | var xhr = getXhr(); 297 | if (request.credentials === 'cors') { 298 | xhr.withCredentials = true; 299 | } 300 | 301 | function responseURL() { 302 | if ('responseURL' in xhr) { 303 | return xhr.responseURL 304 | } 305 | 306 | // Avoid security warnings on getResponseHeader when not allowed by CORS 307 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { 308 | return xhr.getResponseHeader('X-Request-URL') 309 | } 310 | 311 | return; 312 | } 313 | 314 | function onload() { 315 | if (xhr.readyState !== 4) { 316 | return 317 | } 318 | var status = (xhr.status === 1223) ? 204 : xhr.status 319 | if (status < 100 || status > 599) { 320 | reject(new TypeError('Network request failed')) 321 | return 322 | } 323 | var options = { 324 | status: status, 325 | statusText: xhr.statusText, 326 | headers: headers(xhr), 327 | url: responseURL() 328 | } 329 | var body = 'response' in xhr ? xhr.response : xhr.responseText; 330 | resolve(new Response(body, options)) 331 | } 332 | xhr.onreadystatechange = onload; 333 | if (!self.usingActiveXhr) { 334 | xhr.onload = onload; 335 | xhr.onerror = function() { 336 | reject(new TypeError('Network request failed')) 337 | } 338 | } 339 | 340 | xhr.open(request.method, request.url, true) 341 | 342 | if ('responseType' in xhr && support.blob) { 343 | xhr.responseType = 'blob' 344 | } 345 | 346 | request.headers.forEach(function(name, values) { 347 | values.forEach(function(value) { 348 | xhr.setRequestHeader(name, value) 349 | }) 350 | }) 351 | 352 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) 353 | }) 354 | } 355 | fetch.Promise = self.Promise; // you could change it to your favorite alternative 356 | self.fetch.polyfill = true 357 | })(); 358 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-polyfill", 3 | "version": "0.8.2", 4 | "main": "fetch.js", 5 | "repository": "undoZen/fetch", 6 | "licenses": [ 7 | { 8 | "type": "MIT", 9 | "url": "http://mit-license.org/undozen" 10 | } 11 | ], 12 | "devDependencies": { 13 | "browserify": "^9.0.8", 14 | "es6-promise": "^2.1.1", 15 | "isarray": "0.0.1", 16 | "jshint": "2.5.2", 17 | "mocha": "2.1.0" 18 | }, 19 | "files": [ 20 | "LICENSE", 21 | "fetch.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------