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:
parent
116916b98d
commit
16bb6e87ba
|
@ -61,6 +61,7 @@
|
|||
"SyntheticEvent": false,
|
||||
"$Either": false,
|
||||
"$All": false,
|
||||
"$ArrayBufferView": false,
|
||||
"$Tuple": false,
|
||||
"$Supertype": false,
|
||||
"$Subtype": false,
|
||||
|
|
|
@ -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');
|
||||
|
@ -40,6 +41,11 @@ exports.examples = [{
|
|||
render() {
|
||||
return <XHRExampleDownload/>;
|
||||
}
|
||||
}, {
|
||||
title: 'multipart/form-data Upload',
|
||||
render() {
|
||||
return <XHRExampleBinaryUpload/>;
|
||||
}
|
||||
}, {
|
||||
title: 'multipart/form-data Upload',
|
||||
render() {
|
||||
|
|
|
@ -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;
|
|
@ -38,6 +38,8 @@ const {
|
|||
View,
|
||||
} = ReactNative;
|
||||
|
||||
const XHRExampleBinaryUpload = require('./XHRExampleBinaryUpload');
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
class XHRExampleFormData extends React.Component {
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -303,6 +303,11 @@ RCT_EXPORT_MODULE()
|
|||
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) {
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -15,24 +15,13 @@ const NativeEventEmitter = require('NativeEventEmitter');
|
|||
const Platform = require('Platform');
|
||||
const RCTWebSocketModule = require('NativeModules').WebSocketModule;
|
||||
const WebSocketEvent = require('WebSocketEvent');
|
||||
const binaryToBase64 = require('binaryToBase64');
|
||||
|
||||
const EventTarget = require('event-target-shim');
|
||||
const base64 = require('base64-js');
|
||||
|
||||
import type EventSubscription from 'EventSubscription';
|
||||
|
||||
type ArrayBufferView =
|
||||
Int8Array |
|
||||
Uint8Array |
|
||||
Uint8ClampedArray |
|
||||
Int16Array |
|
||||
Uint16Array |
|
||||
Int32Array |
|
||||
Uint32Array |
|
||||
Float32Array |
|
||||
Float64Array |
|
||||
DataView;
|
||||
|
||||
const CONNECTING = 0;
|
||||
const OPEN = 1;
|
||||
const CLOSING = 2;
|
||||
|
@ -108,7 +97,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
this._close(code, reason);
|
||||
}
|
||||
|
||||
send(data: string | ArrayBuffer | ArrayBufferView): void {
|
||||
send(data: string | ArrayBuffer | $ArrayBufferView): void {
|
||||
if (this.readyState === this.CONNECTING) {
|
||||
throw new Error('INVALID_STATE_ERR');
|
||||
}
|
||||
|
@ -118,14 +107,8 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
// $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);
|
||||
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||
RCTWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import okhttp3.Request;
|
|||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* 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_URI = "uri";
|
||||
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 int CHUNK_TIMEOUT_NS = 100 * 1000000; // 100ms
|
||||
private static final int MAX_CHUNK_SIZE_BETWEEN_FLUSHES = 8 * 1024; // 8K
|
||||
|
@ -251,6 +253,20 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
} else {
|
||||
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)) {
|
||||
if (contentType == null) {
|
||||
ResponseUtil.onRequestError(
|
||||
|
|
Loading…
Reference in New Issue