diff --git a/.eslintrc b/.eslintrc
index f1f81f0ff9c..ef11f5e9605 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -61,6 +61,7 @@
"SyntheticEvent": false,
"$Either": false,
"$All": false,
+ "$ArrayBufferView": false,
"$Tuple": false,
"$Supertype": false,
"$Subtype": false,
diff --git a/Examples/UIExplorer/js/XHRExample.js b/Examples/UIExplorer/js/XHRExample.js
index fdb56088b25..5bd0788a4c1 100644
--- a/Examples/UIExplorer/js/XHRExample.js
+++ b/Examples/UIExplorer/js/XHRExample.js
@@ -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 ;
}
+}, {
+ title: 'multipart/form-data Upload',
+ render() {
+ return ;
+ }
}, {
title: 'multipart/form-data Upload',
render() {
diff --git a/Examples/UIExplorer/js/XHRExampleBinaryUpload.js b/Examples/UIExplorer/js/XHRExampleBinaryUpload.js
new file mode 100644
index 00000000000..34c890fcc03
--- /dev/null
+++ b/Examples/UIExplorer/js/XHRExampleBinaryUpload.js
@@ -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 (
+
+ Upload 255 bytes as...
+ this.setState({type})}>
+ {Object.keys(BINARY_TYPES).map((type) => (
+
+ ))}
+
+
+
+
+ Upload
+
+
+
+
+ );
+ }
+
+}
+
+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;
diff --git a/Examples/UIExplorer/js/XHRExampleFormData.js b/Examples/UIExplorer/js/XHRExampleFormData.js
index 9a70b484dc4..04cc5d086b4 100644
--- a/Examples/UIExplorer/js/XHRExampleFormData.js
+++ b/Examples/UIExplorer/js/XHRExampleFormData.js
@@ -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) {
diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js
index 7a0a9073fa2..e6c879bc4e8 100644
--- a/Libraries/Network/RCTNetworking.android.js
+++ b/Libraries/Network/RCTNetworking.android.js
@@ -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 {
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();
diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js
index fca6351dd04..b67049036d5 100644
--- a/Libraries/Network/RCTNetworking.ios.js
+++ b/Libraries/Network/RCTNetworking.ios.js
@@ -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,
diff --git a/Libraries/Network/RCTNetworking.mm b/Libraries/Network/RCTNetworking.mm
index a8aa11a6284..0f6b6455752 100644
--- a/Libraries/Network/RCTNetworking.mm
+++ b/Libraries/Network/RCTNetworking.mm
@@ -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) {
diff --git a/Libraries/Network/convertRequestBody.js b/Libraries/Network/convertRequestBody.js
new file mode 100644
index 00000000000..e82689692bf
--- /dev/null
+++ b/Libraries/Network/convertRequestBody.js
@@ -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;
diff --git a/Libraries/Utilities/binaryToBase64.js b/Libraries/Utilities/binaryToBase64.js
new file mode 100644
index 00000000000..ba23c5c85bd
--- /dev/null
+++ b/Libraries/Utilities/binaryToBase64.js
@@ -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;
diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js
index 7496ef1f9ba..551859e2ee7 100644
--- a/Libraries/WebSocket/WebSocket.js
+++ b/Libraries/WebSocket/WebSocket.js
@@ -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;
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
index bd3062bb047..80f8513a5b6 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java
@@ -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(