mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-16 10:39:50 +08:00
Add support for ontimeout and onerror handler when using XMLHttpRequest for Android and iOS
Summary:Currently React-Native does not have `ontimeout` and `onerror` handlers for [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). This is an extension to [No timeout on XMLHttpRequest](https://github.com/facebook/react-native/issues/4648). With addition to two handlers, both Android and iOS can now handle `ontimeout` if request times out and `onerror` when there is general network error. **Test plan** Code has been tested on both Android and iOS with [Charles](https://www.charlesproxy.com/) by setting a breakpoint on the request which fires `ontimeout` when the request waits beyond `timeout` time and `onerror` when there is network error. **Usage** JavaScript - ``` var request = new XMLHttpRequest(); function onLoad() { console.log(request.status); }; function onTimeout() { console.log('Timeout'); }; function onError() { console.log('General network error'); }; request.onload = onLoad; request.ontimeout = onTimeout; request.onerr Closes https://github.com/facebook/react-native/pull/6841 Differential Revision: D3178859 Pulled By: lexs fb-gh-sync-id: 30674570653e92ab5f7e74bd925dd5640fc862b6 fbshipit-source-id: 30674570653e92ab5f7e74bd925dd5640fc862b6
This commit is contained in:
committed by
Facebook Github Bot 9
parent
967dbd0cbe
commit
d09cd62011
@@ -29,7 +29,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)
|
||||
@@ -297,6 +297,11 @@ exports.examples = [{
|
||||
render() {
|
||||
return <XHRExampleCookies/>;
|
||||
}
|
||||
}, {
|
||||
title: 'Time Out Test',
|
||||
render() {
|
||||
return <XHRExampleOnTimeOut/>;
|
||||
}
|
||||
}];
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
||||
@@ -32,6 +32,7 @@ var {
|
||||
|
||||
var XHRExampleHeaders = require('./XHRExampleHeaders');
|
||||
var XHRExampleFetch = require('./XHRExampleFetch');
|
||||
var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');
|
||||
|
||||
class Downloader extends React.Component {
|
||||
state: any;
|
||||
@@ -331,6 +332,11 @@ exports.examples = [{
|
||||
render() {
|
||||
return <XHRExampleHeaders/>;
|
||||
}
|
||||
}, {
|
||||
title: 'Time Out Test',
|
||||
render() {
|
||||
return <XHRExampleOnTimeOut/>;
|
||||
}
|
||||
}];
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
||||
110
Examples/UIExplorer/XHRExampleOnTimeOut.js
Normal file
110
Examples/UIExplorer/XHRExampleOnTimeOut.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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');
|
||||
var ReactNative = require('react-native');
|
||||
var {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} = ReactNative;
|
||||
|
||||
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;
|
||||
@@ -385,6 +385,7 @@ RCT_EXPORT_MODULE()
|
||||
}
|
||||
NSArray *responseJSON = @[task.requestID,
|
||||
RCTNullIfNil(error.localizedDescription),
|
||||
error.code == kCFURLErrorTimedOut ? @YES : @NO
|
||||
];
|
||||
|
||||
[_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
|
||||
|
||||
@@ -61,6 +61,8 @@ class XMLHttpRequestBase {
|
||||
status: number;
|
||||
timeout: number;
|
||||
responseURL: ?string;
|
||||
ontimeout: ?Function;
|
||||
onerror: ?Function;
|
||||
|
||||
upload: ?{
|
||||
onprogress?: (event: Object) => void;
|
||||
@@ -79,6 +81,7 @@ class XMLHttpRequestBase {
|
||||
_responseType: ResponseType;
|
||||
_sent: boolean;
|
||||
_url: ?string;
|
||||
_timedOut: boolean;
|
||||
|
||||
constructor() {
|
||||
this.UNSENT = UNSENT;
|
||||
@@ -91,11 +94,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._hasError = false;
|
||||
}
|
||||
|
||||
_reset(): void {
|
||||
@@ -115,6 +122,7 @@ class XMLHttpRequestBase {
|
||||
this._lowerCaseResponseHeaders = {};
|
||||
|
||||
this._clearSubscriptions();
|
||||
this._timedOut = false;
|
||||
}
|
||||
|
||||
// $FlowIssue #10784535
|
||||
@@ -249,11 +257,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._hasError = true;
|
||||
if (timeOutError) {
|
||||
this._timedOut = true;
|
||||
}
|
||||
}
|
||||
this._clearSubscriptions();
|
||||
this._requestId = null;
|
||||
@@ -362,17 +373,25 @@ class XMLHttpRequestBase {
|
||||
onreadystatechange.call(this, null);
|
||||
}
|
||||
if (newState === this.DONE && !this._aborted) {
|
||||
this._sendLoad();
|
||||
if (this._hasError) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
Libraries/Network/__tests__/XMLHttpRequestBase-test.js
Normal file
48
Libraries/Network/__tests__/XMLHttpRequestBase-test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.autoMockOff()
|
||||
.dontMock('XMLHttpRequestBase');
|
||||
|
||||
const XMLHttpRequestBase = require('XMLHttpRequestBase');
|
||||
|
||||
describe('XMLHttpRequestBase', function(){
|
||||
var xhr;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = new XMLHttpRequestBase();
|
||||
xhr.ontimeout = jest.fn();
|
||||
xhr.onerror = jest.fn();
|
||||
xhr.onload = jest.fn();
|
||||
xhr.didCreateRequest(1);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
xhr = null;
|
||||
});
|
||||
|
||||
it('should call ontimeout function when the request times out', function(){
|
||||
xhr._didCompleteResponse(1, 'Timeout', true);
|
||||
|
||||
expect(xhr.ontimeout).toBeCalledWith(null);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call onerror function when the request times out', function(){
|
||||
xhr._didCompleteResponse(1, 'Generic error');
|
||||
|
||||
expect(xhr.onerror).toBeCalledWith(null);
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call onload function when there is no error', function(){
|
||||
xhr._didCompleteResponse(1, null);
|
||||
|
||||
expect(xhr.onload).toBeCalledWith(null);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -172,7 +174,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
|
||||
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);
|
||||
@@ -186,7 +188,8 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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);
|
||||
@@ -194,7 +197,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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);
|
||||
@@ -206,14 +209,15 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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(
|
||||
@@ -242,7 +246,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
if (mShuttingDown) {
|
||||
return;
|
||||
}
|
||||
onRequestError(executorToken, requestId, e.getMessage());
|
||||
onRequestError(executorToken, requestId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -264,7 +268,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
onRequestSuccess(executorToken, requestId);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
onRequestError(executorToken, requestId, e.getMessage());
|
||||
onRequestError(executorToken, requestId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -294,11 +298,15 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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 != null) && (e.getClass() == SocketTimeoutException.class)) {
|
||||
args.pushBoolean(true); // last argument is a time out boolean
|
||||
}
|
||||
|
||||
getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args);
|
||||
}
|
||||
|
||||
@@ -385,7 +393,8 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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;
|
||||
@@ -405,7 +414,8 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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);
|
||||
@@ -415,12 +425,13 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
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;
|
||||
@@ -464,4 +475,4 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
return getReactApplicationContext()
|
||||
.getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,34 @@ request.open('GET', 'https://mywebsite.com/endpoint.php');
|
||||
request.send();
|
||||
```
|
||||
|
||||
You can also use -
|
||||
|
||||
```js
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
function onLoad() {
|
||||
console.log(request.status);
|
||||
console.log(request.responseText);
|
||||
};
|
||||
|
||||
function onTimeout() {
|
||||
console.log('Timeout');
|
||||
console.log(request.responseText);
|
||||
};
|
||||
|
||||
function onError() {
|
||||
console.log('General network error');
|
||||
console.log(request.responseText);
|
||||
};
|
||||
|
||||
request.onload = onLoad;
|
||||
request.ontimeout = onTimeout;
|
||||
request.onerror = onError;
|
||||
request.open('GET', 'https://mywebsite.com/endpoint.php');
|
||||
request.send();
|
||||
```
|
||||
|
||||
|
||||
Please follow the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) for a complete description of the API.
|
||||
|
||||
As a developer, you're probably not going to use XMLHttpRequest directly as its API is very tedious to work with. But the fact that it is implemented and compatible with the browser API gives you the ability to use third-party libraries such as [frisbee](https://github.com/niftylettuce/frisbee) or [axios](https://github.com/mzabriskie/axios) directly from npm.
|
||||
|
||||
Reference in New Issue
Block a user