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