├── component.json ├── README.md └── fetch.js /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatwg-fetch", 3 | "description": "This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to uphold this code. [code-of-conduct]: http://todogroup.org/opencodeofconduct/#fetch/opensource@github.com", 4 | "repos": "https://github.com/github/fetch.git", 5 | "main": "fetch.js", 6 | "tag": "master", 7 | "reposType": "npm", 8 | "version": "0.10.1", 9 | "dependencies": {} 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # window.fetch polyfill 2 | 3 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to uphold this code. 4 | [code-of-conduct]: http://todogroup.org/opencodeofconduct/#fetch/opensource@github.com 5 | 6 | The global `fetch` function is an easier way to make web requests and handle 7 | responses than using an XMLHttpRequest. This polyfill is written as closely as 8 | possible to the standard Fetch specification at https://fetch.spec.whatwg.org. 9 | 10 | ## Installation 11 | 12 | Available on [Bower](http://bower.io) as **fetch**. 13 | 14 | ```sh 15 | $ bower install fetch 16 | ``` 17 | 18 | You'll also need a Promise polyfill for [older browsers](http://caniuse.com/#feat=promises). 19 | 20 | ```sh 21 | $ bower install es6-promise 22 | ``` 23 | 24 | This can also be installed with `npm`. 25 | 26 | ```sh 27 | $ npm install whatwg-fetch --save 28 | ``` 29 | 30 | For a node.js implementation, try [node-fetch](https://github.com/bitinn/node-fetch). 31 | 32 | For use with webpack, add this package in the `entry` configuration option before your application entry point: 33 | 34 | ```javascript 35 | entry: ['whatwg-fetch', ...] 36 | ``` 37 | 38 | For babel and es2015+, make sure to import the file: 39 | 40 | ```javascript 41 | import 'whatwg-fetch'; 42 | fetch(...); 43 | ``` 44 | 45 | ## Usage 46 | 47 | The `fetch` function supports any HTTP method. We'll focus on GET and POST 48 | example requests. 49 | 50 | ### HTML 51 | 52 | ```javascript 53 | fetch('/users.html') 54 | .then(function(response) { 55 | return response.text() 56 | }).then(function(body) { 57 | document.body.innerHTML = body 58 | }) 59 | ``` 60 | 61 | ### JSON 62 | 63 | ```javascript 64 | fetch('/users.json') 65 | .then(function(response) { 66 | return response.json() 67 | }).then(function(json) { 68 | console.log('parsed json', json) 69 | }).catch(function(ex) { 70 | console.log('parsing failed', ex) 71 | }) 72 | ``` 73 | 74 | ### Response metadata 75 | 76 | ```javascript 77 | fetch('/users.json').then(function(response) { 78 | console.log(response.headers.get('Content-Type')) 79 | console.log(response.headers.get('Date')) 80 | console.log(response.status) 81 | console.log(response.statusText) 82 | }) 83 | ``` 84 | 85 | ### Post form 86 | 87 | ```javascript 88 | var form = document.querySelector('form') 89 | 90 | fetch('/users', { 91 | method: 'POST', 92 | body: new FormData(form) 93 | }) 94 | ``` 95 | 96 | ### Post JSON 97 | 98 | ```javascript 99 | fetch('/users', { 100 | method: 'POST', 101 | headers: { 102 | 'Accept': 'application/json', 103 | 'Content-Type': 'application/json' 104 | }, 105 | body: JSON.stringify({ 106 | name: 'Hubot', 107 | login: 'hubot', 108 | }) 109 | }) 110 | ``` 111 | 112 | ### File upload 113 | 114 | ```javascript 115 | var input = document.querySelector('input[type="file"]') 116 | 117 | var data = new FormData() 118 | data.append('file', input.files[0]) 119 | data.append('user', 'hubot') 120 | 121 | fetch('/avatars', { 122 | method: 'POST', 123 | body: data 124 | }) 125 | ``` 126 | 127 | ### Caveats 128 | 129 | The `fetch` specification differs from `jQuery.ajax()` in mainly two ways that 130 | bear keeping in mind: 131 | 132 | * The Promise returned from `fetch()` **won't reject on HTTP error status** 133 | even if the response is a HTTP 404 or 500. Instead, it will resolve normally, 134 | and it will only reject on network failure, or if anything prevented the 135 | request from completing. 136 | 137 | * By default, `fetch` **won't send any cookies** to the server, resulting in 138 | unauthenticated requests if the site relies on maintaining a user session. 139 | 140 | #### Handling HTTP error statuses 141 | 142 | To have `fetch` Promise reject on HTTP error statuses, i.e. on any non-2xx 143 | status, define a custom response handler: 144 | 145 | ```javascript 146 | function checkStatus(response) { 147 | if (response.status >= 200 && response.status < 300) { 148 | return response 149 | } else { 150 | var error = new Error(response.statusText) 151 | error.response = response 152 | throw error 153 | } 154 | } 155 | 156 | function parseJSON(response) { 157 | return response.json() 158 | } 159 | 160 | fetch('/users') 161 | .then(checkStatus) 162 | .then(parseJSON) 163 | .then(function(data) { 164 | console.log('request succeeded with JSON response', data) 165 | }).catch(function(error) { 166 | console.log('request failed', error) 167 | }) 168 | ``` 169 | 170 | #### Sending cookies 171 | 172 | To automatically send cookies for the current domain, the `credentials` option 173 | must be provided: 174 | 175 | ```javascript 176 | fetch('/users', { 177 | credentials: 'same-origin' 178 | }) 179 | ``` 180 | 181 | This option makes `fetch` behave similar to XMLHttpRequest with regards to 182 | cookies. Otherwise, cookies won't get sent, resulting in these requests not 183 | preserving the authentication session. 184 | 185 | Use the `include` value to send cookies in a [cross-origin resource sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) (CORS) request. 186 | 187 | ```javascript 188 | fetch('https://example.com:1234/users', { 189 | credentials: 'include' 190 | }) 191 | ``` 192 | 193 | 194 | #### Receiving cookies 195 | 196 | Like with XMLHttpRequest, the `Set-Cookie` response header returned from the 197 | server is a [forbidden header name][] and therefore can't be programatically 198 | read with `response.headers.get()`. Instead, it's the browser's responsibility 199 | to handle new cookies being set (if applicable to the current URL). Unless they 200 | are HTTP-only, new cookies will be available through `document.cookie`. 201 | 202 | [forbidden header name]: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name 203 | 204 | #### Obtaining the Response URL 205 | 206 | Due to limitations of XMLHttpRequest, the `response.url` value might not be 207 | reliable after HTTP redirects on older browsers. 208 | 209 | The solution is to configure the server to set the response HTTP header 210 | `X-Request-URL` to the current URL after any redirect that might have happened. 211 | It should be safe to set it unconditionally. 212 | 213 | ``` ruby 214 | # Ruby on Rails controller example 215 | response.headers['X-Request-URL'] = request.url 216 | ``` 217 | 218 | This server workaround is necessary if you need reliable `response.url` in 219 | Firefox < 32, Chrome < 37, Safari, or IE. 220 | 221 | ## Browser Support 222 | 223 | ![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) 224 | --- | --- | --- | --- | --- | 225 | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | 226 | -------------------------------------------------------------------------------- /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 = String(name) 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 = String(value) 21 | } 22 | return value 23 | } 24 | 25 | function Headers(headers) { 26 | this.map = {} 27 | 28 | if (headers instanceof Headers) { 29 | headers.forEach(function(value, name) { 30 | this.append(name, value) 31 | }, this) 32 | 33 | } else if (headers) { 34 | Object.getOwnPropertyNames(headers).forEach(function(name) { 35 | this.append(name, headers[name]) 36 | }, this) 37 | } 38 | } 39 | 40 | Headers.prototype.append = function(name, value) { 41 | name = normalizeName(name) 42 | value = normalizeValue(value) 43 | var list = this.map[name] 44 | if (!list) { 45 | list = [] 46 | this.map[name] = list 47 | } 48 | list.push(value) 49 | } 50 | 51 | Headers.prototype['delete'] = function(name) { 52 | delete this.map[normalizeName(name)] 53 | } 54 | 55 | Headers.prototype.get = function(name) { 56 | var values = this.map[normalizeName(name)] 57 | return values ? values[0] : null 58 | } 59 | 60 | Headers.prototype.getAll = function(name) { 61 | return this.map[normalizeName(name)] || [] 62 | } 63 | 64 | Headers.prototype.has = function(name) { 65 | return this.map.hasOwnProperty(normalizeName(name)) 66 | } 67 | 68 | Headers.prototype.set = function(name, value) { 69 | this.map[normalizeName(name)] = [normalizeValue(value)] 70 | } 71 | 72 | Headers.prototype.forEach = function(callback, thisArg) { 73 | Object.getOwnPropertyNames(this.map).forEach(function(name) { 74 | this.map[name].forEach(function(value) { 75 | callback.call(thisArg, value, name, this) 76 | }, this) 77 | }, this) 78 | } 79 | 80 | function consumed(body) { 81 | if (body.bodyUsed) { 82 | return Promise.reject(new TypeError('Already read')) 83 | } 84 | body.bodyUsed = true 85 | } 86 | 87 | function fileReaderReady(reader) { 88 | return new Promise(function(resolve, reject) { 89 | reader.onload = function() { 90 | resolve(reader.result) 91 | } 92 | reader.onerror = function() { 93 | reject(reader.error) 94 | } 95 | }) 96 | } 97 | 98 | function readBlobAsArrayBuffer(blob) { 99 | var reader = new FileReader() 100 | reader.readAsArrayBuffer(blob) 101 | return fileReaderReady(reader) 102 | } 103 | 104 | function readBlobAsText(blob) { 105 | var reader = new FileReader() 106 | reader.readAsText(blob) 107 | return fileReaderReady(reader) 108 | } 109 | 110 | var support = { 111 | blob: 'FileReader' in self && 'Blob' in self && (function() { 112 | try { 113 | new Blob(); 114 | return true 115 | } catch(e) { 116 | return false 117 | } 118 | })(), 119 | formData: 'FormData' in self, 120 | arrayBuffer: 'ArrayBuffer' in self 121 | } 122 | 123 | function Body() { 124 | this.bodyUsed = false 125 | 126 | 127 | this._initBody = function(body) { 128 | this._bodyInit = body 129 | if (typeof body === 'string') { 130 | this._bodyText = body 131 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { 132 | this._bodyBlob = body 133 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { 134 | this._bodyFormData = body 135 | } else if (!body) { 136 | this._bodyText = '' 137 | } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) { 138 | // Only support ArrayBuffers for POST method. 139 | // Receiving ArrayBuffers happens via Blobs, instead. 140 | } else { 141 | throw new Error('unsupported BodyInit type') 142 | } 143 | } 144 | 145 | if (support.blob) { 146 | this.blob = function() { 147 | var rejected = consumed(this) 148 | if (rejected) { 149 | return rejected 150 | } 151 | 152 | if (this._bodyBlob) { 153 | return Promise.resolve(this._bodyBlob) 154 | } else if (this._bodyFormData) { 155 | throw new Error('could not read FormData body as blob') 156 | } else { 157 | return Promise.resolve(new Blob([this._bodyText])) 158 | } 159 | } 160 | 161 | this.arrayBuffer = function() { 162 | return this.blob().then(readBlobAsArrayBuffer) 163 | } 164 | 165 | this.text = function() { 166 | var rejected = consumed(this) 167 | if (rejected) { 168 | return rejected 169 | } 170 | 171 | if (this._bodyBlob) { 172 | return readBlobAsText(this._bodyBlob) 173 | } else if (this._bodyFormData) { 174 | throw new Error('could not read FormData body as text') 175 | } else { 176 | return Promise.resolve(this._bodyText) 177 | } 178 | } 179 | } else { 180 | this.text = function() { 181 | var rejected = consumed(this) 182 | return rejected ? rejected : Promise.resolve(this._bodyText) 183 | } 184 | } 185 | 186 | if (support.formData) { 187 | this.formData = function() { 188 | return this.text().then(decode) 189 | } 190 | } 191 | 192 | this.json = function() { 193 | return this.text().then(JSON.parse) 194 | } 195 | 196 | return this 197 | } 198 | 199 | // HTTP methods whose capitalization should be normalized 200 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] 201 | 202 | function normalizeMethod(method) { 203 | var upcased = method.toUpperCase() 204 | return (methods.indexOf(upcased) > -1) ? upcased : method 205 | } 206 | 207 | function Request(input, options) { 208 | options = options || {} 209 | var body = options.body 210 | if (Request.prototype.isPrototypeOf(input)) { 211 | if (input.bodyUsed) { 212 | throw new TypeError('Already read') 213 | } 214 | this.url = input.url 215 | this.credentials = input.credentials 216 | if (!options.headers) { 217 | this.headers = new Headers(input.headers) 218 | } 219 | this.method = input.method 220 | this.mode = input.mode 221 | if (!body) { 222 | body = input._bodyInit 223 | input.bodyUsed = true 224 | } 225 | } else { 226 | this.url = input 227 | } 228 | 229 | this.credentials = options.credentials || this.credentials || 'omit' 230 | if (options.headers || !this.headers) { 231 | this.headers = new Headers(options.headers) 232 | } 233 | this.method = normalizeMethod(options.method || this.method || 'GET') 234 | this.mode = options.mode || this.mode || null 235 | this.referrer = null 236 | 237 | if ((this.method === 'GET' || this.method === 'HEAD') && body) { 238 | throw new TypeError('Body not allowed for GET or HEAD requests') 239 | } 240 | this._initBody(body) 241 | } 242 | 243 | Request.prototype.clone = function() { 244 | return new Request(this) 245 | } 246 | 247 | function decode(body) { 248 | var form = new FormData() 249 | body.trim().split('&').forEach(function(bytes) { 250 | if (bytes) { 251 | var split = bytes.split('=') 252 | var name = split.shift().replace(/\+/g, ' ') 253 | var value = split.join('=').replace(/\+/g, ' ') 254 | form.append(decodeURIComponent(name), decodeURIComponent(value)) 255 | } 256 | }) 257 | return form 258 | } 259 | 260 | function headers(xhr) { 261 | var head = new Headers() 262 | var pairs = xhr.getAllResponseHeaders().trim().split('\n') 263 | pairs.forEach(function(header) { 264 | var split = header.trim().split(':') 265 | var key = split.shift().trim() 266 | var value = split.join(':').trim() 267 | head.append(key, value) 268 | }) 269 | return head 270 | } 271 | 272 | Body.call(Request.prototype) 273 | 274 | function Response(bodyInit, options) { 275 | if (!options) { 276 | options = {} 277 | } 278 | 279 | this._initBody(bodyInit) 280 | this.type = 'default' 281 | this.status = options.status 282 | this.ok = this.status >= 200 && this.status < 300 283 | this.statusText = options.statusText 284 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) 285 | this.url = options.url || '' 286 | } 287 | 288 | Body.call(Response.prototype) 289 | 290 | Response.prototype.clone = function() { 291 | return new Response(this._bodyInit, { 292 | status: this.status, 293 | statusText: this.statusText, 294 | headers: new Headers(this.headers), 295 | url: this.url 296 | }) 297 | } 298 | 299 | Response.error = function() { 300 | var response = new Response(null, {status: 0, statusText: ''}) 301 | response.type = 'error' 302 | return response 303 | } 304 | 305 | var redirectStatuses = [301, 302, 303, 307, 308] 306 | 307 | Response.redirect = function(url, status) { 308 | if (redirectStatuses.indexOf(status) === -1) { 309 | throw new RangeError('Invalid status code') 310 | } 311 | 312 | return new Response(null, {status: status, headers: {location: url}}) 313 | } 314 | 315 | self.Headers = Headers; 316 | self.Request = Request; 317 | self.Response = Response; 318 | 319 | self.fetch = function(input, init) { 320 | return new Promise(function(resolve, reject) { 321 | var request 322 | if (Request.prototype.isPrototypeOf(input) && !init) { 323 | request = input 324 | } else { 325 | request = new Request(input, init) 326 | } 327 | 328 | var xhr = new XMLHttpRequest() 329 | 330 | function responseURL() { 331 | if ('responseURL' in xhr) { 332 | return xhr.responseURL 333 | } 334 | 335 | // Avoid security warnings on getResponseHeader when not allowed by CORS 336 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { 337 | return xhr.getResponseHeader('X-Request-URL') 338 | } 339 | 340 | return; 341 | } 342 | 343 | xhr.onload = function() { 344 | var status = (xhr.status === 1223) ? 204 : xhr.status 345 | if (status < 100 || status > 599) { 346 | reject(new TypeError('Network request failed')) 347 | return 348 | } 349 | var options = { 350 | status: status, 351 | statusText: xhr.statusText, 352 | headers: headers(xhr), 353 | url: responseURL() 354 | } 355 | var body = 'response' in xhr ? xhr.response : xhr.responseText; 356 | resolve(new Response(body, options)) 357 | } 358 | 359 | xhr.onerror = function() { 360 | reject(new TypeError('Network request failed')) 361 | } 362 | 363 | xhr.open(request.method, request.url, true) 364 | 365 | if (request.credentials === 'include') { 366 | xhr.withCredentials = true 367 | } 368 | 369 | if ('responseType' in xhr && support.blob) { 370 | xhr.responseType = 'blob' 371 | } 372 | 373 | request.headers.forEach(function(value, name) { 374 | xhr.setRequestHeader(name, value) 375 | }) 376 | 377 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) 378 | }) 379 | } 380 | self.fetch.polyfill = true 381 | })(); 382 | --------------------------------------------------------------------------------