Add responseType as a concept to RCTNetworking, send binary data as base64

Summary:
In preparation for Blob support (wherein binary XHR and WebSocket responses can be retained as native data blobs on the native side and JS receives a web-like opaque Blob object), this change makes RCTNetworking aware of the responseType that JS requests. A `xhr.responseType` of `''` or `'text'` translates to a native response type of `'text'`. A `xhr.responseType` of `arraybuffer` translates to a native response type of `base64`, as we currently lack an API to transmit TypedArrays directly to JS. This is analogous to how the WebSocket module already works, and it's a lot more versatile and much less brittle than converting a JS *string* back to a TypedArray, which is what's currently going on.

Now that we don't always send text down to JS, JS consumers might still want to get progress updates about a binary download. This is what the `'progress'` event is designed for, so this change also implements that. This change also follows the XHR spec with regards to `xhr.response` and `xhr.responseText`:

- if the response type is `'text'`, `xhr.responseText` can be peeked at by the JS consumer. It will be updated periodically as the download progresses, so long as there's either an `onreadystatechange` or `onprogress` handler on the XHR.

- if the response type is not `'text'`, `xhr.responseText` can't be accessed and `xhr.response` remains `null` until the response is fully received. `'progress'` events containing response details (total bytes, downloaded so far) are dispatched if there's an `onprogress` handler.

Once Blobs are landed, `xhr.responseType` of `'blob'` will correspond to the same native response type, which will cause RCTNetworking to only send a blob ID down to JS, which can then create a `Blob` object from that for consumers.

Closes https://github.com/facebook/react-native/pull/8324

Reviewed By: javache

Differential Revision: D3508822

Pulled By: davidaurelio

fbshipit-source-id: 441b2d4d40265b6036559c3ccb9fa962999fa5df
This commit is contained in:
Philipp von Weitershausen
2016-07-13 04:53:54 -07:00
committed by Facebook Github Bot 0
parent c65eb4ef19
commit 08c375f828
18 changed files with 849 additions and 368 deletions

View File

@@ -9,6 +9,8 @@
package com.facebook.react.modules.network;
import android.util.Base64;
import javax.annotation.Nullable;
import java.io.IOException;
@@ -34,6 +36,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.JavaNetCookieJar;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@@ -157,6 +160,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
final int requestId,
ReadableArray headers,
ReadableMap data,
final String responseType,
final boolean useIncrementalUpdates,
int timeout) {
Request.Builder requestBuilder = new Request.Builder().url(url);
@@ -165,18 +169,54 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
requestBuilder.tag(requestId);
}
OkHttpClient client = mClient;
final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken);
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
// If JS is listening for progress updates, install a ProgressResponseBody that intercepts the
// response and counts bytes received.
if (useIncrementalUpdates) {
clientBuilder.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
ProgressResponseBody responseBody = new ProgressResponseBody(
originalResponse.body(),
new ProgressListener() {
long last = System.nanoTime();
@Override
public void onProgress(long bytesWritten, long contentLength, boolean done) {
long now = System.nanoTime();
if (!done && !shouldDispatch(now, last)) {
return;
}
if (responseType.equals("text")) {
// For 'text' responses we continuously send response data with progress info to
// JS below, so no need to do anything here.
return;
}
ResponseUtil.onDataReceivedProgress(
eventEmitter,
requestId,
bytesWritten,
contentLength);
last = now;
}
});
return originalResponse.newBuilder().body(responseBody).build();
}
});
}
// If the current timeout does not equal the passed in timeout, we need to clone the existing
// client and set the timeout explicitly on the clone. This is cheap as everything else is
// shared under the hood.
// See https://github.com/square/okhttp/wiki/Recipes#per-call-configuration for more information
if (timeout != mClient.connectTimeoutMillis()) {
client = mClient.newBuilder()
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.build();
clientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS);
}
OkHttpClient client = clientBuilder.build();
final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken);
Headers requestHeaders = extractHeaders(headers, data);
if (requestHeaders == null) {
ResponseUtil.onRequestError(eventEmitter, requestId, "Unrecognized headers format", null);
@@ -247,11 +287,11 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
method,
RequestBodyUtil.createProgressRequest(
multipartBuilder.build(),
new ProgressRequestListener() {
new ProgressListener() {
long last = System.nanoTime();
@Override
public void onRequestProgress(long bytesWritten, long contentLength, boolean done) {
public void onProgress(long bytesWritten, long contentLength, boolean done) {
long now = System.nanoTime();
if (done || shouldDispatch(now, last)) {
ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength);
@@ -292,13 +332,23 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
ResponseBody responseBody = response.body();
try {
if (useIncrementalUpdates) {
// If JS wants progress updates during the download, and it requested a text response,
// periodically send response data updates to JS.
if (useIncrementalUpdates && responseType.equals("text")) {
readWithProgress(eventEmitter, requestId, responseBody);
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
} else {
ResponseUtil.onDataReceived(eventEmitter, requestId, responseBody.string());
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
return;
}
// Otherwise send the data in one big chunk, in the format that JS requested.
String responseString = "";
if (responseType.equals("text")) {
responseString = responseBody.string();
} else if (responseType.equals("base64")) {
responseString = Base64.encodeToString(responseBody.bytes(), Base64.NO_WRAP);
}
ResponseUtil.onDataReceived(eventEmitter, requestId, responseString);
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
} catch (IOException e) {
ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e);
}
@@ -310,12 +360,27 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
RCTDeviceEventEmitter eventEmitter,
int requestId,
ResponseBody responseBody) throws IOException {
long totalBytesRead = -1;
long contentLength = -1;
try {
ProgressResponseBody progressResponseBody = (ProgressResponseBody) responseBody;
totalBytesRead = progressResponseBody.totalBytesRead();
contentLength = progressResponseBody.contentLength();
} catch (ClassCastException e) {
// Ignore
}
Reader reader = responseBody.charStream();
try {
char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
int read;
while ((read = reader.read(buffer)) != -1) {
ResponseUtil.onDataReceived(eventEmitter, requestId, new String(buffer, 0, read));
ResponseUtil.onIncrementalDataReceived(
eventEmitter,
requestId,
new String(buffer, 0, read),
totalBytesRead,
contentLength);
}
} finally {
reader.close();

View File

@@ -6,10 +6,9 @@
* 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.
*/
package com.facebook.react.modules.network;
public interface ProgressRequestListener {
void onRequestProgress(long bytesWritten, long contentLength, boolean done);
public interface ProgressListener {
void onProgress(long bytesWritten, long contentLength, boolean done);
}

View File

@@ -12,22 +12,19 @@ package com.facebook.react.modules.network;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.internal.Util;
import okio.BufferedSink;
import okio.Buffer;
import okio.Sink;
import okio.ForwardingSink;
import okio.ByteString;
import okio.Okio;
import okio.Source;
public class ProgressRequestBody extends RequestBody {
private final RequestBody mRequestBody;
private final ProgressRequestListener mProgressListener;
private final ProgressListener mProgressListener;
private BufferedSink mBufferedSink;
public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
mRequestBody = requestBody;
mProgressListener = progressListener;
}
@@ -63,7 +60,8 @@ public class ProgressRequestBody extends RequestBody {
contentLength = contentLength();
}
bytesWritten += byteCount;
mProgressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
mProgressListener.onProgress(
bytesWritten, contentLength, bytesWritten == contentLength);
}
};
}

View File

@@ -0,0 +1,62 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.modules.network;
import java.io.IOException;
import javax.annotation.Nullable;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;
public class ProgressResponseBody extends ResponseBody {
private final ResponseBody mResponseBody;
private final ProgressListener mProgressListener;
private @Nullable BufferedSource mBufferedSource;
private long mTotalBytesRead;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.mResponseBody = responseBody;
this.mProgressListener = progressListener;
mTotalBytesRead = 0L;
}
@Override
public MediaType contentType() {
return mResponseBody.contentType();
}
@Override
public long contentLength() {
return mResponseBody.contentLength();
}
public long totalBytesRead() {
return mTotalBytesRead;
}
@Override public BufferedSource source() {
if (mBufferedSource == null) {
mBufferedSource = Okio.buffer(source(mResponseBody.source()));
}
return mBufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted.
mTotalBytesRead += bytesRead != -1 ? bytesRead : 0;
mProgressListener.onProgress(
mTotalBytesRead, mResponseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}

View File

@@ -117,7 +117,9 @@ import okio.Source;
/**
* Creates a ProgressRequestBody that can be used for showing uploading progress
*/
public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressRequestListener listener) {
public static ProgressRequestBody createProgressRequest(
RequestBody requestBody,
ProgressListener listener) {
return new ProgressRequestBody(requestBody, listener);
}

View File

@@ -33,6 +33,34 @@ public class ResponseUtil {
eventEmitter.emit("didSendNetworkData", args);
}
public static void onIncrementalDataReceived(
RCTDeviceEventEmitter eventEmitter,
int requestId,
String data,
long progress,
long total) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushString(data);
args.pushInt((int) progress);
args.pushInt((int) total);
eventEmitter.emit("didReceiveNetworkIncrementalData", args);
}
public static void onDataReceivedProgress(
RCTDeviceEventEmitter eventEmitter,
int requestId,
long progress,
long total) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushInt((int) progress);
args.pushInt((int) total);
eventEmitter.emit("didReceiveNetworkDataProgress", args);
}
public static void onDataReceived(
RCTDeviceEventEmitter eventEmitter,
int requestId,