diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js
index 55b1a51787c841..af136bbecd161c 100644
--- a/Examples/UIExplorer/XHRExample.android.js
+++ b/Examples/UIExplorer/XHRExample.android.js
@@ -28,7 +28,7 @@ var {
var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleCookies = require('./XHRExampleCookies');
var XHRExampleFetch = require('./XHRExampleFetch');
-
+var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');
// TODO t7093728 This is a simplified XHRExample.ios.js.
// Once we have Camera roll, Toast, Intent (for opening URLs)
@@ -296,6 +296,11 @@ exports.examples = [{
render() {
return ;
}
+}, {
+ title: 'Time Out Test',
+ render() {
+ return ;
+ }
}];
var styles = StyleSheet.create({
diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js
index 3045e2f033286c..d34f86da2fded0 100644
--- a/Examples/UIExplorer/XHRExample.ios.js
+++ b/Examples/UIExplorer/XHRExample.ios.js
@@ -31,6 +31,7 @@ var {
var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleFetch = require('./XHRExampleFetch');
+var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');
class Downloader extends React.Component {
state: any;
@@ -330,6 +331,11 @@ exports.examples = [{
render() {
return ;
}
+}, {
+ title: 'Time Out Test',
+ render() {
+ return ;
+ }
}];
var styles = StyleSheet.create({
diff --git a/Examples/UIExplorer/XHRExampleOnTimeOut.js b/Examples/UIExplorer/XHRExampleOnTimeOut.js
new file mode 100644
index 00000000000000..6293359b8a7e47
--- /dev/null
+++ b/Examples/UIExplorer/XHRExampleOnTimeOut.js
@@ -0,0 +1,109 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ View,
+} = React;
+
+class XHRExampleOnTimeOut extends React.Component {
+ state: any;
+ xhr: XMLHttpRequest;
+
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ status: '',
+ loading: false
+ };
+ }
+
+ loadTimeOutRequest() {
+ this.xhr && this.xhr.abort();
+
+ var xhr = this.xhr || new XMLHttpRequest();
+
+ xhr.onerror = ()=> {
+ console.log('Status ', xhr.status);
+ console.log('Error ', xhr.responseText);
+ };
+
+ xhr.ontimeout = () => {
+ this.setState({
+ status: xhr.responseText,
+ loading: false
+ });
+ };
+
+ xhr.onload = () => {
+ console.log('Status ', xhr.status);
+ console.log('Response ', xhr.responseText);
+ }
+
+ xhr.open('GET', 'https://httpbin.org/delay/5'); // request to take 5 seconds to load
+ xhr.timeout = 2000; // request times out in 2 seconds
+ xhr.send();
+ this.xhr = xhr;
+
+ this.setState({loading: true});
+ }
+
+ componentWillUnmount() {
+ this.xhr && this.xhr.abort();
+ }
+
+ render() {
+ var button = this.state.loading ? (
+
+
+ Loading...
+
+
+ ) : (
+
+
+ Make Time Out Request
+
+
+ );
+
+ return (
+
+ {button}
+ {this.state.status}
+
+ );
+ }
+}
+
+var styles = StyleSheet.create({
+ wrapper: {
+ borderRadius: 5,
+ marginBottom: 5,
+ },
+ button: {
+ backgroundColor: '#eeeeee',
+ padding: 8,
+ },
+});
+
+module.exports = XHRExampleOnTimeOut;
diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m
index 88c2de0e06fe42..07b128fd996514 100644
--- a/Libraries/Network/RCTNetworking.m
+++ b/Libraries/Network/RCTNetworking.m
@@ -385,6 +385,7 @@ - (void)sendRequest:(NSURLRequest *)request
}
NSArray *responseJSON = @[task.requestID,
RCTNullIfNil(error.localizedDescription),
+ error.code == kCFURLErrorTimedOut ? @YES : @NO
];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js
index e82ce515afbd54..1adc07787cca1d 100644
--- a/Libraries/Network/XMLHttpRequestBase.js
+++ b/Libraries/Network/XMLHttpRequestBase.js
@@ -49,6 +49,8 @@ class XMLHttpRequestBase {
status: number;
timeout: number;
responseURL: ?string;
+ ontimeout: ?Function;
+ onerror: ?Function;
upload: ?{
onprogress?: (event: Object) => void;
@@ -63,6 +65,8 @@ class XMLHttpRequestBase {
_sent: boolean;
_aborted: boolean;
_lowerCaseResponseHeaders: Object;
+ _timedOut: boolean;
+ _error: boolean;
constructor() {
this.UNSENT = UNSENT;
@@ -75,11 +79,15 @@ class XMLHttpRequestBase {
this.onload = null;
this.upload = undefined; /* Upload not supported yet */
this.timeout = 0;
+ this.ontimeout = null;
+ this.onerror = null;
this._reset();
this._method = null;
this._url = null;
this._aborted = false;
+ this._timedOut = false;
+ this._error = false;
}
_reset() {
@@ -98,6 +106,8 @@ class XMLHttpRequestBase {
this._lowerCaseResponseHeaders = {};
this._clearSubscriptions();
+ this._timedOut = false;
+ this._error = false;
}
didCreateRequest(requestId: number): void {
@@ -171,10 +181,14 @@ class XMLHttpRequestBase {
}
}
- _didCompleteResponse(requestId: number, error: string): void {
+ _didCompleteResponse(requestId: number, error: string, timeOutError: boolean): void {
if (requestId === this._requestId) {
if (error) {
this.responseText = error;
+ this._error = true;
+ if (timeOutError) {
+ this._timedOut = true;
+ }
}
this._clearSubscriptions();
this._requestId = null;
@@ -249,7 +263,7 @@ class XMLHttpRequestBase {
abort(): void {
this._aborted = true;
if (this._requestId) {
- RCTNetworking.abortRequest(this._requestId);
+ RCTNetworking.abortRequest(this._requestId);_
}
// only call onreadystatechange if there is something to abort,
// below logic is per spec
@@ -283,17 +297,25 @@ class XMLHttpRequestBase {
onreadystatechange.call(this, null);
}
if (newState === this.DONE && !this._aborted) {
- this._sendLoad();
+ if (this._error) {
+ if (this._timedOut) {
+ this._sendEvent(this.ontimeout);
+ } else {
+ this._sendEvent(this.onerror);
+ }
+ }
+ else {
+ this._sendEvent(this.onload);
+ }
}
}
- _sendLoad(): void {
+ _sendEvent(newEvent: ?Function): void {
// TODO: workaround flow bug with nullable function checks
- var onload = this.onload;
- if (onload) {
+ if (newEvent) {
// We should send an event to handler, but since we don't process that
// event anywhere, let's leave it empty
- onload(null);
+ newEvent(null);
}
}
}
@@ -304,4 +326,4 @@ XMLHttpRequestBase.HEADERS_RECEIVED = HEADERS_RECEIVED;
XMLHttpRequestBase.LOADING = LOADING;
XMLHttpRequestBase.DONE = DONE;
-module.exports = XMLHttpRequestBase;
+module.exports = XMLHttpRequestBase;
\ No newline at end of file
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
index 066dbd0a4a899e..c1e8332daf9d2b 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
@@ -15,6 +15,8 @@
import java.io.InputStream;
import java.io.Reader;
+import java.net.SocketTimeoutException;
+
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -175,7 +177,7 @@ public void sendRequest(
Headers requestHeaders = extractHeaders(headers, data);
if (requestHeaders == null) {
- onRequestError(executorToken, requestId, "Unrecognized headers format");
+ onRequestError(executorToken, requestId, "Unrecognized headers format", null);
return;
}
String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME);
@@ -189,7 +191,8 @@ public void sendRequest(
onRequestError(
executorToken,
requestId,
- "Payload is set but no content-type header specified");
+ "Payload is set but no content-type header specified",
+ null);
return;
}
String body = data.getString(REQUEST_BODY_KEY_STRING);
@@ -197,7 +200,7 @@ public void sendRequest(
if (RequestBodyUtil.isGzipEncoding(contentEncoding)) {
RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
if (requestBody == null) {
- onRequestError(executorToken, requestId, "Failed to gzip request body");
+ onRequestError(executorToken, requestId, "Failed to gzip request body", null);
return;
}
requestBuilder.method(method, requestBody);
@@ -209,14 +212,15 @@ public void sendRequest(
onRequestError(
executorToken,
requestId,
- "Payload is set but no content-type header specified");
+ "Payload is set but no content-type header specified",
+ null);
return;
}
String uri = data.getString(REQUEST_BODY_KEY_URI);
InputStream fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri);
if (fileInputStream == null) {
- onRequestError(executorToken, requestId, "Could not retrieve file for uri " + uri);
+ onRequestError(executorToken, requestId, "Could not retrieve file for uri " + uri, null);
return;
}
requestBuilder.method(
@@ -245,7 +249,7 @@ public void onFailure(Request request, IOException e) {
if (mShuttingDown) {
return;
}
- onRequestError(executorToken, requestId, e.getMessage());
+ onRequestError(executorToken, requestId, e.getMessage(), e);
}
@Override
@@ -267,7 +271,7 @@ public void onResponse(Response response) throws IOException {
onRequestSuccess(executorToken, requestId);
}
} catch (IOException e) {
- onRequestError(executorToken, requestId, e.getMessage());
+ onRequestError(executorToken, requestId, e.getMessage(), e);
}
}
});
@@ -322,11 +326,15 @@ private void onDataReceived(ExecutorToken ExecutorToken, int requestId, String d
getEventEmitter(ExecutorToken).emit("didReceiveNetworkData", args);
}
- private void onRequestError(ExecutorToken ExecutorToken, int requestId, String error) {
+ private void onRequestError(ExecutorToken ExecutorToken, int requestId, String error, IOException e) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushString(error);
+ if (e.getClass() == SocketTimeoutException.class) {
+ args.pushBoolean(true); // last argument is a time out boolean
+ }
+
getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args);
}
@@ -413,7 +421,8 @@ MultipartBuilder constructMultipartBody(
onRequestError(
ExecutorToken,
requestId,
- "Missing or invalid header format for FormData part.");
+ "Missing or invalid header format for FormData part.",
+ null);
return null;
}
MediaType partContentType = null;
@@ -433,7 +442,8 @@ MultipartBuilder constructMultipartBody(
onRequestError(
ExecutorToken,
requestId,
- "Binary FormData part needs a content-type header.");
+ "Binary FormData part needs a content-type header.",
+ null);
return null;
}
String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI);
@@ -443,12 +453,13 @@ MultipartBuilder constructMultipartBody(
onRequestError(
ExecutorToken,
requestId,
- "Could not retrieve file for uri " + fileContentUriStr);
+ "Could not retrieve file for uri " + fileContentUriStr,
+ null);
return null;
}
multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream));
} else {
- onRequestError(ExecutorToken, requestId, "Unrecognized FormData part.");
+ onRequestError(ExecutorToken, requestId, "Unrecognized FormData part.", null);
}
}
return multipartBuilder;
@@ -492,4 +503,4 @@ private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter(ExecutorT
return getReactApplicationContext()
.getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class);
}
-}
+}
\ No newline at end of file