From 8f6afcf53d0860d3886982aa0fed01bfd5d405b2 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 16 Mar 2017 15:31:23 -0400 Subject: [PATCH 1/2] fetch timeout polyfill --- README.md | 44 +++++++++++++++++++++++++ fetch-polyfill.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 README.md create mode 100644 fetch-polyfill.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e3c7f4 --- /dev/null +++ b/README.md @@ -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. diff --git a/fetch-polyfill.js b/fetch-polyfill.js new file mode 100644 index 0000000..86541a7 --- /dev/null +++ b/fetch-polyfill.js @@ -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) + }) +} From 942a97257b98b8d55cb9529aaa9add9c38af878d Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 16 Mar 2017 16:04:35 -0400 Subject: [PATCH 2/2] package file updates --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2dd9c02..5bba9aa 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,14 @@ "fetch", "timeout" ], - "author": "Atticus White (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" },