diff --git a/Examples/UIExplorer/UIExplorerList.windows.js b/Examples/UIExplorer/UIExplorerList.windows.js index 0e7b9e3818d..580beb5d77c 100644 --- a/Examples/UIExplorer/UIExplorerList.windows.js +++ b/Examples/UIExplorer/UIExplorerList.windows.js @@ -112,6 +112,10 @@ const APIExamples = [ key: 'WebSocketExample', module: require('./WebSocketExample'), }, + { + key: 'XHRExample', + module: require('./XHRExample'), + }, ]; const Modules = {}; diff --git a/Examples/UIExplorer/XHRExample.windows.js b/Examples/UIExplorer/XHRExample.windows.js new file mode 100644 index 00000000000..4ceca78a40c --- /dev/null +++ b/Examples/UIExplorer/XHRExample.windows.js @@ -0,0 +1,351 @@ +/** + * 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, + TextInput, + TouchableHighlight, + View, +} = ReactNative; + +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) +// we should make this consistent with iOS. + +class Downloader extends React.Component { + + xhr: XMLHttpRequest; + cancelled: boolean; + + constructor(props) { + super(props); + this.cancelled = false; + this.state = { + status: '', + contentSize: 1, + downloaded: 0, + }; + } + + download() { + this.xhr && this.xhr.abort(); + + var xhr = this.xhr || new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === xhr.HEADERS_RECEIVED) { + var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10); + this.setState({ + contentSize: contentSize, + downloaded: 0, + }); + } else if (xhr.readyState === xhr.LOADING) { + this.setState({ + downloaded: xhr.responseText.length, + }); + } else if (xhr.readyState === xhr.DONE) { + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + this.setState({ + status: 'Download complete!', + }); + } else if (xhr.status !== 0) { + this.setState({ + status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText, + }); + } else { + this.setState({ + status: 'Error: ' + xhr.responseText, + }); + } + } + }; + xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt'); + // Avoid gzip so we can actually show progress + xhr.setRequestHeader('Accept-Encoding', ''); + xhr.send(); + this.xhr = xhr; + + this.setState({status: 'Downloading...'}); + } + + componentWillUnmount() { + this.cancelled = true; + this.xhr && this.xhr.abort(); + } + + render() { + var button = this.state.status === 'Downloading...' ? ( + + + ... + + + ) : ( + + + Download 5MB Text File + + + ); + + return ( + + {button} + {this.state.status} + + ); + } +} + +class FormUploader extends React.Component { + + _isMounted: boolean; + _addTextParam: () => void; + _upload: () => void; + + constructor(props) { + super(props); + this.state = { + isUploading: false, + uploadProgress: null, + textParams: [], + }; + this._isMounted = true; + this._addTextParam = this._addTextParam.bind(this); + this._upload = this._upload.bind(this); + } + + _addTextParam() { + var textParams = this.state.textParams; + textParams.push({name: '', value: ''}); + this.setState({textParams}); + } + + componentWillUnmount() { + this._isMounted = false; + } + + _onTextParamNameChange(index, text) { + var textParams = this.state.textParams; + textParams[index].name = text; + this.setState({textParams}); + } + + _onTextParamValueChange(index, text) { + var textParams = this.state.textParams; + textParams[index].value = text; + this.setState({textParams}); + } + + _upload() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'http://posttestserver.com/post.php'); + xhr.onload = () => { + this.setState({isUploading: false}); + if (xhr.status !== 200) { + console.log( + 'Upload failed', + 'Expected HTTP 200 OK response, got ' + xhr.status + ); + return; + } + if (!xhr.responseText) { + console.log( + 'Upload failed', + 'No response payload.' + ); + return; + } + var index = xhr.responseText.indexOf('http://www.posttestserver.com/'); + if (index === -1) { + console.log( + 'Upload failed', + 'Invalid response payload.' + ); + return; + } + var url = xhr.responseText.slice(index).split('\n')[0]; + console.log('Upload successful: ' + url); + }; + var formdata = new FormData(); + this.state.textParams.forEach( + (param) => formdata.append(param.name, param.value) + ); + if (xhr.upload) { + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + } + xhr.send(formdata); + this.setState({isUploading: true}); + } + + render() { + var textItems = this.state.textParams.map((item, index) => ( + + + = + + + )); + var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload'; + var uploadProgress = this.state.uploadProgress; + if (uploadProgress !== null) { + uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%'; + } + var uploadButton = ( + + {uploadButtonLabel} + + ); + if (!this.state.isUploading) { + uploadButton = ( + + {uploadButton} + + ); + } + return ( + + {textItems} + + + Add a text param + + + + {uploadButton} + + + ); + } +} + +exports.framework = 'React'; +exports.title = 'XMLHttpRequest'; +exports.description = 'Example that demonstrates upload and download requests ' + + 'using XMLHttpRequest.'; +exports.examples = [{ + title: 'File Download', + render() { + return ; + } +}, { + title: 'multipart/form-data Upload', + render() { + return ; + } +}, { + title: 'Fetch Test', + render() { + return ; + } +}, { + title: 'Headers', + render() { + return ; + } +}, { + title: 'Cookies', + render() { + return ; + } +}, { + title: 'Time Out Test', + render() { + return ; + } +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 8, + }, + paramRow: { + flexDirection: 'row', + paddingVertical: 8, + alignItems: 'center', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: 'grey', + }, + textButton: { + color: 'blue', + }, + addTextParamButton: { + marginTop: 8, + }, + textInput: { + flex: 1, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + paddingLeft: 8, + }, + equalSign: { + paddingHorizontal: 4, + }, + uploadButton: { + marginTop: 16, + }, + uploadButtonBox: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + backgroundColor: 'blue', + borderRadius: 4, + }, + uploadButtonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/ReactWindows/ReactNative/Modules/Network/NetworkingModule.cs b/ReactWindows/ReactNative/Modules/Network/NetworkingModule.cs index d425867ab11..039eedd01d0 100644 --- a/ReactWindows/ReactNative/Modules/Network/NetworkingModule.cs +++ b/ReactWindows/ReactNative/Modules/Network/NetworkingModule.cs @@ -112,7 +112,7 @@ public void sendRequest( { if (headerData.ContentType == null) { - OnRequestError(requestId, "Payload is set but no 'content-type' header specified."); + OnRequestError(requestId, "Payload is set but no 'content-type' header specified.", false); return; } @@ -124,14 +124,16 @@ public void sendRequest( } else if ((formData = data.Value("formData")) != null) { + // TODO: (#388) Add support for form data. throw new NotImplementedException("HTTP handling for FormData not yet implemented."); } } _tasks.Add(requestId, token => ProcessRequestAsync( - requestId, - useIncrementalUpdates, - request, + requestId, + useIncrementalUpdates, + timeout, + request, token)); } @@ -154,53 +156,71 @@ public override void OnReactInstanceDispose() } private async Task ProcessRequestAsync( - int requestId, - bool useIncrementalUpdates, + int requestId, + bool useIncrementalUpdates, + int timeout, HttpRequestMessage request, CancellationToken token) { - try + var timeoutSource = timeout > 0 + ? new CancellationTokenSource(timeout) + : new CancellationTokenSource(); + + using (timeoutSource) { - using (var response = await _client.SendRequestAsync(request, token)) + try { - OnResponseReceived(requestId, response); - - if (useIncrementalUpdates) + using (token.Register(timeoutSource.Cancel)) + using (var response = await _client.SendRequestAsync(request, timeoutSource.Token)) { - using (var inputStream = await response.Content.ReadAsInputStreamAsync()) - using (var stream = inputStream.AsStreamForRead()) + OnResponseReceived(requestId, response); + + if (useIncrementalUpdates) { - await ProcessResponseIncrementalAsync(requestId, stream, token); - OnRequestSuccess(requestId); + using (var inputStream = await response.Content.ReadAsInputStreamAsync()) + using (var stream = inputStream.AsStreamForRead()) + { + await ProcessResponseIncrementalAsync(requestId, stream, timeoutSource.Token); + OnRequestSuccess(requestId); + } } - } - else - { - if (response.Content != null) + else { - var responseBody = await response.Content.ReadAsStringAsync(); - if (responseBody != null) + if (response.Content != null) { - OnDataReceived(requestId, responseBody); + var responseBody = await response.Content.ReadAsStringAsync(); + if (responseBody != null) + { + OnDataReceived(requestId, responseBody); + } } - } - OnRequestSuccess(requestId); + OnRequestSuccess(requestId); + } } } - } - catch (Exception ex) - { - if (_shuttingDown) + catch (OperationCanceledException ex) + when (ex.CancellationToken == timeoutSource.Token) { - return; + // Cancellation was due to timeout + if (!token.IsCancellationRequested) + { + OnRequestError(requestId, ex.Message, true); + } } + catch (Exception ex) + { + if (_shuttingDown) + { + return; + } - OnRequestError(requestId, ex.Message); - } - finally - { - request.Dispose(); + OnRequestError(requestId, ex.Message, false); + } + finally + { + request.Dispose(); + } } } @@ -264,12 +284,13 @@ private void OnDataReceived(int requestId, string responseBody) }); } - private void OnRequestError(int requestId, string message) + private void OnRequestError(int requestId, string message, bool timeout) { EventEmitter.emit("didCompleteNetworkResponse", new JArray { requestId, message, + timeout }); }