Skip to content

Commit

Permalink
Add support for ontimeout and onerror handler when using XMLHttpRequest
Browse files Browse the repository at this point in the history
  • Loading branch information
grgmo committed Apr 9, 2016
1 parent 29a1a05 commit ab924b8
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 22 deletions.
7 changes: 6 additions & 1 deletion Examples/UIExplorer/XHRExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -296,6 +296,11 @@ exports.examples = [{
render() {
return <XHRExampleCookies/>;
}
}, {
title: 'Time Out Test',
render() {
return <XHRExampleOnTimeOut/>;
}
}];

var styles = StyleSheet.create({
Expand Down
6 changes: 6 additions & 0 deletions Examples/UIExplorer/XHRExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var {

var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleFetch = require('./XHRExampleFetch');
var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');

class Downloader extends React.Component {
state: any;
Expand Down Expand Up @@ -330,6 +331,11 @@ exports.examples = [{
render() {
return <XHRExampleHeaders/>;
}
}, {
title: 'Time Out Test',
render() {
return <XHRExampleOnTimeOut/>;
}
}];

var styles = StyleSheet.create({
Expand Down
109 changes: 109 additions & 0 deletions Examples/UIExplorer/XHRExampleOnTimeOut.js
Original file line number Diff line number Diff line change
@@ -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 ? (
<View style={styles.wrapper}>
<View style={styles.button}>
<Text>Loading...</Text>
</View>
</View>
) : (
<TouchableHighlight
style={styles.wrapper}
onPress={this.loadTimeOutRequest.bind(this)}>
<View style={styles.button}>
<Text>Make Time Out Request</Text>
</View>
</TouchableHighlight>
);

return (
<View>
{button}
<Text>{this.state.status}</Text>
</View>
);
}
}

var styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
marginBottom: 5,
},
button: {
backgroundColor: '#eeeeee',
padding: 8,
},
});

module.exports = XHRExampleOnTimeOut;
1 change: 1 addition & 0 deletions Libraries/Network/RCTNetworking.m
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ - (void)sendRequest:(NSURLRequest *)request
}
NSArray *responseJSON = @[task.requestID,
RCTNullIfNil(error.localizedDescription),
error.code == kCFURLErrorTimedOut ? @YES : @NO
];

[_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
Expand Down
38 changes: 30 additions & 8 deletions Libraries/Network/XMLHttpRequestBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class XMLHttpRequestBase {
status: number;
timeout: number;
responseURL: ?string;
ontimeout: ?Function;
onerror: ?Function;
upload: ?{
onprogress?: (event: Object) => void;
Expand All @@ -63,6 +65,8 @@ class XMLHttpRequestBase {
_sent: boolean;
_aborted: boolean;
_lowerCaseResponseHeaders: Object;
_timedOut: boolean;
_error: boolean;
constructor() {
this.UNSENT = UNSENT;
Expand All @@ -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() {
Expand All @@ -98,6 +106,8 @@ class XMLHttpRequestBase {
this._lowerCaseResponseHeaders = {};
this._clearSubscriptions();
this._timedOut = false;
this._error = false;
}
didCreateRequest(requestId: number): void {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -304,4 +326,4 @@ XMLHttpRequestBase.HEADERS_RECEIVED = HEADERS_RECEIVED;
XMLHttpRequestBase.LOADING = LOADING;
XMLHttpRequestBase.DONE = DONE;

module.exports = XMLHttpRequestBase;
module.exports = XMLHttpRequestBase;
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -189,15 +191,16 @@ 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);
MediaType contentMediaType = MediaType.parse(contentType);
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);
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
});
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -492,4 +503,4 @@ private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter(ExecutorT
return getReactApplicationContext()
.getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class);
}
}
}

0 comments on commit ab924b8

Please sign in to comment.