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
});
}