Skip to content

Commit

Permalink
XHR: support typed arrays for request payloads
Browse files Browse the repository at this point in the history
Summary:
Support `xhr.send(data)` for typed arrays.

**Test plan:** run UIExplorer example on iOS and Android.
Closes facebook#11904

Differential Revision: D4425551

fbshipit-source-id: 065ab5873407a406ca4a831068ab138606c3361b
  • Loading branch information
philikon authored and normanjoyner committed Feb 9, 2017
1 parent 588c6e6 commit 330e426
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 63 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"SyntheticEvent": false,
"$Either": false,
"$All": false,
"$ArrayBufferView": false,
"$Tuple": false,
"$Supertype": false,
"$Subtype": false,
Expand Down
6 changes: 6 additions & 0 deletions Examples/UIExplorer/js/XHRExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
var React = require('react');

var XHRExampleDownload = require('./XHRExampleDownload');
var XHRExampleBinaryUpload = require('./XHRExampleBinaryUpload');
var XHRExampleFormData = require('./XHRExampleFormData');
var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleFetch = require('./XHRExampleFetch');
Expand All @@ -40,6 +41,11 @@ exports.examples = [{
render() {
return <XHRExampleDownload/>;
}
}, {
title: 'multipart/form-data Upload',
render() {
return <XHRExampleBinaryUpload/>;
}
}, {
title: 'multipart/form-data Upload',
render() {
Expand Down
170 changes: 170 additions & 0 deletions Examples/UIExplorer/js/XHRExampleBinaryUpload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* 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';

const React = require('react');
const ReactNative = require('react-native');
const {
Alert,
Linking,
Picker,
StyleSheet,
Text,
TouchableHighlight,
View,
} = ReactNative;

const BINARY_TYPES = {
String,
ArrayBuffer,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
DataView,
};

const SAMPLE_TEXT = `
I am the spirit that negates.
And rightly so, for all that comes to be
Deserves to perish wretchedly;
'Twere better nothing would begin.
Thus everything that that your terms, sin,
Destruction, evil represent—
That is my proper element.
--Faust, JW Goethe
`;


class XHRExampleBinaryUpload extends React.Component {

static handlePostTestServerUpload(xhr: XMLHttpRequest) {
if (xhr.status !== 200) {
Alert.alert(
'Upload failed',
'Expected HTTP 200 OK response, got ' + xhr.status
);
return;
}
if (!xhr.responseText) {
Alert.alert(
'Upload failed',
'No response payload.'
);
return;
}
var index = xhr.responseText.indexOf('http://www.posttestserver.com/');
if (index === -1) {
Alert.alert(
'Upload failed',
'Invalid response payload.'
);
return;
}
var url = xhr.responseText.slice(index).split('\n')[0];
console.log('Upload successful: ' + url);
Linking.openURL(url);
}

state = {
type: 'Uint8Array',
};

_upload = () => {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://posttestserver.com/post.php');
xhr.onload = () => XHRExampleBinaryUpload.handlePostTestServerUpload(xhr);
xhr.setRequestHeader('Content-Type', 'text/plain');

if (this.state.type === 'String') {
xhr.send(SAMPLE_TEXT);
return;
}

const arrayBuffer = new ArrayBuffer(256);
const asBytes = new Uint8Array(arrayBuffer);
for (let i = 0; i < SAMPLE_TEXT.length; i++) {
asBytes[i] = SAMPLE_TEXT.charCodeAt(i);
}
if (this.state.type === 'ArrayBuffer') {
xhr.send(arrayBuffer);
return;
}
if (this.state.type === 'Uint8Array') {
xhr.send(asBytes);
return;
}

const TypedArrayClass = BINARY_TYPES[this.state.type];
xhr.send(new TypedArrayClass(arrayBuffer));
};

render() {
return (
<View>
<Text>Upload 255 bytes as...</Text>
<Picker
selectedValue={this.state.type}
onValueChange={(type) => this.setState({type})}>
{Object.keys(BINARY_TYPES).map((type) => (
<Picker.Item key={type} label={type} value={type} />
))}
</Picker>
<View style={styles.uploadButton}>
<TouchableHighlight onPress={this._upload}>
<View style={styles.uploadButtonBox}>
<Text style={styles.uploadButtonLabel}>Upload</Text>
</View>
</TouchableHighlight>
</View>
</View>
);
}

}

const styles = StyleSheet.create({
uploadButton: {
marginTop: 16,
},
uploadButtonBox: {
flex: 1,
paddingVertical: 12,
alignItems: 'center',
backgroundColor: 'blue',
borderRadius: 4,
},
uploadButtonLabel: {
color: 'white',
fontSize: 16,
fontWeight: '500',
},
});

module.exports = XHRExampleBinaryUpload;
28 changes: 3 additions & 25 deletions Examples/UIExplorer/js/XHRExampleFormData.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const {
View,
} = ReactNative;

const XHRExampleBinaryUpload = require('./XHRExampleBinaryUpload');

const PAGE_SIZE = 20;

class XHRExampleFormData extends React.Component {
Expand Down Expand Up @@ -109,31 +111,7 @@ class XHRExampleFormData extends React.Component {
xhr.open('POST', 'http://posttestserver.com/post.php');
xhr.onload = () => {
this.setState({isUploading: false});
if (xhr.status !== 200) {
Alert.alert(
'Upload failed',
'Expected HTTP 200 OK response, got ' + xhr.status
);
return;
}
if (!xhr.responseText) {
Alert.alert(
'Upload failed',
'No response payload.'
);
return;
}
var index = xhr.responseText.indexOf('http://www.posttestserver.com/');
if (index === -1) {
Alert.alert(
'Upload failed',
'Invalid response payload.'
);
return;
}
var url = xhr.responseText.slice(index).split('\n')[0];
console.log('Upload successful: ' + url);
Linking.openURL(url);
XHRExampleBinaryUpload.handlePostTestServerUpload(xhr);
};
var formdata = new FormData();
if (this.state.randomPhoto) {
Expand Down
25 changes: 13 additions & 12 deletions Libraries/Network/RCTNetworking.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
const FormData = require('FormData');
const NativeEventEmitter = require('NativeEventEmitter');
const RCTNetworkingNative = require('NativeModules').Networking;
const convertRequestBody = require('convertRequestBody');

import type {RequestBody} from 'convertRequestBody';

type Header = [string, string];

// Convert FormData headers to arrays, which are easier to consume in
// native on Android.
function convertHeadersMapToArray(headers: Object): Array<Header> {
const headerArray = [];
for (const name in headers) {
Expand Down Expand Up @@ -47,16 +52,19 @@ class RCTNetworking extends NativeEventEmitter {
trackingName: string,
url: string,
headers: Object,
data: string | FormData | {uri: string},
data: RequestBody,
responseType: 'text' | 'base64',
incrementalUpdates: boolean,
timeout: number,
callback: (requestId: number) => any
) {
const body =
typeof data === 'string' ? {string: data} :
data instanceof FormData ? {formData: getParts(data)} :
data;
const body = convertRequestBody(data);
if (body && body.formData) {
body.formData = body.formData.map((part) => ({
...part,
headers: convertHeadersMapToArray(part.headers),
}));
}
const requestId = generateRequestId();
RCTNetworkingNative.sendRequest(
method,
Expand All @@ -80,11 +88,4 @@ class RCTNetworking extends NativeEventEmitter {
}
}

function getParts(data) {
return data.getParts().map((part) => {
part.headers = convertHeadersMapToArray(part.headers);
return part;
});
}

module.exports = new RCTNetworking();
10 changes: 5 additions & 5 deletions Libraries/Network/RCTNetworking.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
const FormData = require('FormData');
const NativeEventEmitter = require('NativeEventEmitter');
const RCTNetworkingNative = require('NativeModules').Networking;
const convertRequestBody = require('convertRequestBody');

import type {RequestBody} from 'convertRequestBody';

class RCTNetworking extends NativeEventEmitter {

Expand All @@ -26,16 +29,13 @@ class RCTNetworking extends NativeEventEmitter {
trackingName: string,
url: string,
headers: Object,
data: string | FormData | {uri: string},
data: RequestBody,
responseType: 'text' | 'base64',
incrementalUpdates: boolean,
timeout: number,
callback: (requestId: number) => any
) {
const body =
typeof data === 'string' ? {string: data} :
data instanceof FormData ? {formData: data.getParts()} :
data;
const body = convertRequestBody(data);
RCTNetworkingNative.sendRequest({
method,
url,
Expand Down
5 changes: 5 additions & 0 deletions Libraries/Network/RCTNetworking.mm
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ - (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(nullable NSDictionary
if (body) {
return callback(nil, @{@"body": body});
}
NSString *base64String = [RCTConvert NSString:query[@"base64"]];
if (base64String) {
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
return callback(nil, @{@"body": data});
}
NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]];
if (request) {

Expand Down
40 changes: 40 additions & 0 deletions Libraries/Network/convertRequestBody.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule convertRequestBody
* @flow
*/
'use strict';

const binaryToBase64 = require('binaryToBase64');

const FormData = require('FormData');

export type RequestBody =
string
| FormData
| {uri: string}
| ArrayBuffer
| $ArrayBufferView
;

function convertRequestBody(body: RequestBody): Object {
if (typeof body === 'string') {
return {string: body};
}
if (body instanceof FormData) {
return {formData: body.getParts()};
}
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
// $FlowFixMe: no way to assert that 'body' is indeed an ArrayBufferView
return {base64: binaryToBase64(body)};
}
return body;
}

module.exports = convertRequestBody;
Loading

0 comments on commit 330e426

Please sign in to comment.