XHR: support typed arrays for request payloads

Summary:
Support `xhr.send(data)` for typed arrays.

**Test plan:** run UIExplorer example on iOS and Android.
Closes https://github.com/facebook/react-native/pull/11904

Differential Revision: D4425551

fbshipit-source-id: 065ab5873407a406ca4a831068ab138606c3361b
This commit is contained in:
Philipp von Weitershausen 2017-01-20 18:40:28 -08:00 committed by Facebook Github Bot
parent 116916b98d
commit 16bb6e87ba
11 changed files with 293 additions and 63 deletions

View File

@ -61,6 +61,7 @@
"SyntheticEvent": false, "SyntheticEvent": false,
"$Either": false, "$Either": false,
"$All": false, "$All": false,
"$ArrayBufferView": false,
"$Tuple": false, "$Tuple": false,
"$Supertype": false, "$Supertype": false,
"$Subtype": false, "$Subtype": false,

View File

@ -25,6 +25,7 @@
var React = require('react'); var React = require('react');
var XHRExampleDownload = require('./XHRExampleDownload'); var XHRExampleDownload = require('./XHRExampleDownload');
var XHRExampleBinaryUpload = require('./XHRExampleBinaryUpload');
var XHRExampleFormData = require('./XHRExampleFormData'); var XHRExampleFormData = require('./XHRExampleFormData');
var XHRExampleHeaders = require('./XHRExampleHeaders'); var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleFetch = require('./XHRExampleFetch'); var XHRExampleFetch = require('./XHRExampleFetch');
@ -40,6 +41,11 @@ exports.examples = [{
render() { render() {
return <XHRExampleDownload/>; return <XHRExampleDownload/>;
} }
}, {
title: 'multipart/form-data Upload',
render() {
return <XHRExampleBinaryUpload/>;
}
}, { }, {
title: 'multipart/form-data Upload', title: 'multipart/form-data Upload',
render() { render() {

View File

@ -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;

View File

@ -38,6 +38,8 @@ const {
View, View,
} = ReactNative; } = ReactNative;
const XHRExampleBinaryUpload = require('./XHRExampleBinaryUpload');
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
class XHRExampleFormData extends React.Component { class XHRExampleFormData extends React.Component {
@ -109,31 +111,7 @@ class XHRExampleFormData extends React.Component {
xhr.open('POST', 'http://posttestserver.com/post.php'); xhr.open('POST', 'http://posttestserver.com/post.php');
xhr.onload = () => { xhr.onload = () => {
this.setState({isUploading: false}); this.setState({isUploading: false});
if (xhr.status !== 200) { XHRExampleBinaryUpload.handlePostTestServerUpload(xhr);
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);
}; };
var formdata = new FormData(); var formdata = new FormData();
if (this.state.randomPhoto) { if (this.state.randomPhoto) {

View File

@ -16,9 +16,14 @@
const FormData = require('FormData'); const FormData = require('FormData');
const NativeEventEmitter = require('NativeEventEmitter'); const NativeEventEmitter = require('NativeEventEmitter');
const RCTNetworkingNative = require('NativeModules').Networking; const RCTNetworkingNative = require('NativeModules').Networking;
const convertRequestBody = require('convertRequestBody');
import type {RequestBody} from 'convertRequestBody';
type Header = [string, string]; type Header = [string, string];
// Convert FormData headers to arrays, which are easier to consume in
// native on Android.
function convertHeadersMapToArray(headers: Object): Array<Header> { function convertHeadersMapToArray(headers: Object): Array<Header> {
const headerArray = []; const headerArray = [];
for (const name in headers) { for (const name in headers) {
@ -47,16 +52,19 @@ class RCTNetworking extends NativeEventEmitter {
trackingName: string, trackingName: string,
url: string, url: string,
headers: Object, headers: Object,
data: string | FormData | {uri: string}, data: RequestBody,
responseType: 'text' | 'base64', responseType: 'text' | 'base64',
incrementalUpdates: boolean, incrementalUpdates: boolean,
timeout: number, timeout: number,
callback: (requestId: number) => any callback: (requestId: number) => any
) { ) {
const body = const body = convertRequestBody(data);
typeof data === 'string' ? {string: data} : if (body && body.formData) {
data instanceof FormData ? {formData: getParts(data)} : body.formData = body.formData.map((part) => ({
data; ...part,
headers: convertHeadersMapToArray(part.headers),
}));
}
const requestId = generateRequestId(); const requestId = generateRequestId();
RCTNetworkingNative.sendRequest( RCTNetworkingNative.sendRequest(
method, method,
@ -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(); module.exports = new RCTNetworking();

View File

@ -14,6 +14,9 @@
const FormData = require('FormData'); const FormData = require('FormData');
const NativeEventEmitter = require('NativeEventEmitter'); const NativeEventEmitter = require('NativeEventEmitter');
const RCTNetworkingNative = require('NativeModules').Networking; const RCTNetworkingNative = require('NativeModules').Networking;
const convertRequestBody = require('convertRequestBody');
import type {RequestBody} from 'convertRequestBody';
class RCTNetworking extends NativeEventEmitter { class RCTNetworking extends NativeEventEmitter {
@ -26,16 +29,13 @@ class RCTNetworking extends NativeEventEmitter {
trackingName: string, trackingName: string,
url: string, url: string,
headers: Object, headers: Object,
data: string | FormData | {uri: string}, data: RequestBody,
responseType: 'text' | 'base64', responseType: 'text' | 'base64',
incrementalUpdates: boolean, incrementalUpdates: boolean,
timeout: number, timeout: number,
callback: (requestId: number) => any callback: (requestId: number) => any
) { ) {
const body = const body = convertRequestBody(data);
typeof data === 'string' ? {string: data} :
data instanceof FormData ? {formData: data.getParts()} :
data;
RCTNetworkingNative.sendRequest({ RCTNetworkingNative.sendRequest({
method, method,
url, url,

View File

@ -303,6 +303,11 @@ RCT_EXPORT_MODULE()
if (body) { if (body) {
return callback(nil, @{@"body": 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"]]; NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]];
if (request) { if (request) {

View File

@ -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;

View File

@ -0,0 +1,30 @@
/**
* 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 binaryToBase64
* @flow
*/
'use strict';
const base64 = require('base64-js');
function binaryToBase64(data: ArrayBuffer | $ArrayBufferView) {
if (data instanceof ArrayBuffer) {
data = new Uint8Array(data);
}
if (data instanceof Uint8Array) {
return base64.fromByteArray(data);
}
if (!ArrayBuffer.isView(data)) {
throw new Error('data must be ArrayBuffer or typed array');
}
const {buffer, byteOffset, byteLength} = data;
return base64.fromByteArray(new Uint8Array(buffer, byteOffset, byteLength));
}
module.exports = binaryToBase64;

View File

@ -15,24 +15,13 @@ const NativeEventEmitter = require('NativeEventEmitter');
const Platform = require('Platform'); const Platform = require('Platform');
const RCTWebSocketModule = require('NativeModules').WebSocketModule; const RCTWebSocketModule = require('NativeModules').WebSocketModule;
const WebSocketEvent = require('WebSocketEvent'); const WebSocketEvent = require('WebSocketEvent');
const binaryToBase64 = require('binaryToBase64');
const EventTarget = require('event-target-shim'); const EventTarget = require('event-target-shim');
const base64 = require('base64-js'); const base64 = require('base64-js');
import type EventSubscription from 'EventSubscription'; import type EventSubscription from 'EventSubscription';
type ArrayBufferView =
Int8Array |
Uint8Array |
Uint8ClampedArray |
Int16Array |
Uint16Array |
Int32Array |
Uint32Array |
Float32Array |
Float64Array |
DataView;
const CONNECTING = 0; const CONNECTING = 0;
const OPEN = 1; const OPEN = 1;
const CLOSING = 2; const CLOSING = 2;
@ -108,7 +97,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
this._close(code, reason); this._close(code, reason);
} }
send(data: string | ArrayBuffer | ArrayBufferView): void { send(data: string | ArrayBuffer | $ArrayBufferView): void {
if (this.readyState === this.CONNECTING) { if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR'); throw new Error('INVALID_STATE_ERR');
} }
@ -118,14 +107,8 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
return; return;
} }
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
if (ArrayBuffer.isView(data)) { RCTWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
// $FlowFixMe: no way to assert that 'data' is indeed an ArrayBufferView now
data = data.buffer;
}
if (data instanceof ArrayBuffer) {
data = base64.fromByteArray(new Uint8Array(data));
RCTWebSocketModule.sendBinary(data, this._socketId);
return; return;
} }

View File

@ -46,6 +46,7 @@ import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import okio.ByteString;
/** /**
* Implements the XMLHttpRequest JavaScript interface. * Implements the XMLHttpRequest JavaScript interface.
@ -58,6 +59,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
private static final String REQUEST_BODY_KEY_STRING = "string"; private static final String REQUEST_BODY_KEY_STRING = "string";
private static final String REQUEST_BODY_KEY_URI = "uri"; private static final String REQUEST_BODY_KEY_URI = "uri";
private static final String REQUEST_BODY_KEY_FORMDATA = "formData"; private static final String REQUEST_BODY_KEY_FORMDATA = "formData";
private static final String REQUEST_BODY_KEY_BASE64 = "base64";
private static final String USER_AGENT_HEADER_NAME = "user-agent"; private static final String USER_AGENT_HEADER_NAME = "user-agent";
private static final int CHUNK_TIMEOUT_NS = 100 * 1000000; // 100ms private static final int CHUNK_TIMEOUT_NS = 100 * 1000000; // 100ms
private static final int MAX_CHUNK_SIZE_BETWEEN_FLUSHES = 8 * 1024; // 8K private static final int MAX_CHUNK_SIZE_BETWEEN_FLUSHES = 8 * 1024; // 8K
@ -251,6 +253,20 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} else { } else {
requestBuilder.method(method, RequestBody.create(contentMediaType, body)); requestBuilder.method(method, RequestBody.create(contentMediaType, body));
} }
} else if (data.hasKey(REQUEST_BODY_KEY_BASE64)) {
if (contentType == null) {
ResponseUtil.onRequestError(
eventEmitter,
requestId,
"Payload is set but no content-type header specified",
null);
return;
}
String base64String = data.getString(REQUEST_BODY_KEY_BASE64);
MediaType contentMediaType = MediaType.parse(contentType);
requestBuilder.method(
method,
RequestBody.create(contentMediaType, ByteString.decodeBase64(base64String)));
} else if (data.hasKey(REQUEST_BODY_KEY_URI)) { } else if (data.hasKey(REQUEST_BODY_KEY_URI)) {
if (contentType == null) { if (contentType == null) {
ResponseUtil.onRequestError( ResponseUtil.onRequestError(