Skip to content

Commit

Permalink
Merge pull request #1 from robinpowered/polyfill
Browse files Browse the repository at this point in the history
Fetch timeout polyfill
  • Loading branch information
christophermark authored Mar 17, 2017
2 parents 6c628e2 + 942a972 commit 8f0f1c5
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 2 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
A polyfill for React Native's `whatwg-fetch`'s mirror.

### The polyfill

This adds support for `timeout` as one of the `fetch` options.

```js
import fetch from 'react-native-fetch-polyfill';

fetch(url, {timeout: 30 * 1000})
.then(response => {
// a successful response
})
.catch(error => {
// an error when the request fails, such as during a timeout
})
```

React Native's `XMLHttpRequest` interface [exposes a timeout property sent to the `RCTNetworking` module](https://github.com/facebook/react-native/blob/v0.42.1/Libraries/Network/XMLHttpRequest.js#L500), as well as an [abort method](https://github.com/facebook/react-native/blob/v0.42.1/Libraries/Network/XMLHttpRequest.js#L505-L520). `fetch` does not expose access to this by default, this polyfill allows specifying a `timeout` within the options.

This value [attached to `NSMutableURLRequest`](https://github.com/facebook/react-native/blob/v0.42.1/Libraries/Network/RCTNetworking.mm#L232), where the native networking layer will enforce the timeout rule.

The result of the timeout being reached will result in a promise [rejected with a `TypeError('Network rqeuest failed')](https://github.com/github/fetch/blob/v1.1.1/fetch.js#L445).


### What is fetch?

Fetch is a networking abstraction above [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It reflects the [WHATWG fetch specification](https://fetch.spec.whatwg.org/) and can be found in [whatwg/fetch](https://github.com/whatwg/fetch). It is the networking library [used in React Native](https://facebook.github.io/react-native/docs/network.html#using-fetch).

### Why a polyfill?

Fetch has two challenges:
- It cannot be externally aborted (https://github.com/whatwg/fetch/issues/27 and https://github.com/whatwg/fetch/issues/447)
- It does not support `timeout`(https://github.com/facebook/react-native/issues/2394, https://github.com/facebook/react-native/issues/2556, https://github.com/whatwg/fetch/issues/20, https://github.com/github/fetch/issues/175)

Why are these not supported? As a `fetch` maintainer points out in https://github.com/github/fetch/pull/68#issuecomment-70103306, the spec does not describe a standard for this behavior.

### How is the polyfill maintained?

The polyfill picks out specific pieces of [whatwg/fetch](https://github.com/whatwg/fetch) required to apply the patch.

The tagged version of the polyfill corresponds to the version of `fetch` that it patches.

When new versions of `fetch` are released, the polyfill will be updated and tagged.
81 changes: 81 additions & 0 deletions fetch-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

// Polyfill from https://github.com/github/fetch/blob/v1.1.1/fetch.js#L8-L21
var support = {
searchParams: 'URLSearchParams' in self,
iterable: 'Symbol' in self && 'iterator' in Symbol,
blob: 'FileReader' in self && 'Blob' in self && (function() {
try {
new Blob()
return true
} catch(e) {
return false
}
})(),
formData: 'FormData' in self,
arrayBuffer: 'ArrayBuffer' in self
}

// Polyfill from https://github.com/github/fetch/blob/v1.1.1/fetch.js#L364-L375
function parseHeaders(rawHeaders) {
var headers = new Headers()
rawHeaders.split(/\r?\n/).forEach(function(line) {
var parts = line.split(':')
var key = parts.shift().trim()
if (key) {
var value = parts.join(':').trim()
headers.append(key, value)
}
});

return headers;
}

// Polyfill from https://github.com/github/fetch/blob/v1.1.1/fetch.js#L424-L464
export default function fetchPolyfill (input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init)
var xhr = new XMLHttpRequest()

/* @patch: timeout */
if (init.timeout) {
xhr.timeout = init.timeout;
}
/* @endpatch */

xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
var body = 'response' in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options))
}

xhr.onerror = function() {
reject(new TypeError('Network request failed'))
}

xhr.ontimeout = function() {
reject(new TypeError('Network request failed'))
}

xhr.open(request.method, request.url, true)

if (request.credentials === 'include') {
xhr.withCredentials = true
}

if ('responseType' in xhr && support.blob) {
xhr.responseType = 'blob'
}

request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value)
})

xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@
"fetch",
"timeout"
],
"author": "Atticus White <contact@atticuswhite.com> (http://atticuswhite.com/)",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.31"
},
"contributors": [{
"name": "Atticus White",
"email": "atticus@robinpowered.com"
}],
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/robinpowered/react-native-fetch-polyfill/issues"
},
Expand Down

0 comments on commit 8f0f1c5

Please sign in to comment.