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:58:37 -07:00
committed by Facebook Github Bot 0
parent c65eb4ef19
commit 08c375f828
18 changed files with 849 additions and 368 deletions
@@ -61,11 +61,12 @@ import static org.mockito.Mockito.when;
Call.class,
RequestBodyUtil.class,
ProgressRequestBody.class,
ProgressRequestListener.class,
ProgressListener.class,
MultipartBody.class,
MultipartBody.Builder.class,
NetworkingModule.class,
OkHttpClient.class,
OkHttpClient.Builder.class,
OkHttpCallUtil.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@@ -84,6 +85,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
@@ -91,11 +95,12 @@ public class NetworkingModuleTest {
mock(ExecutorToken.class),
"GET",
"http://somedomain/foo",
0,
JavaOnlyArray.of(),
null,
true,
0);
/* requestId */ 0,
/* headers */ JavaOnlyArray.of(),
/* body */ null,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
@@ -112,6 +117,9 @@ public class NetworkingModuleTest {
when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class);
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
List<JavaOnlyArray> invalidHeaders = Arrays.asList(JavaOnlyArray.of("foo"));
@@ -122,11 +130,12 @@ public class NetworkingModuleTest {
mock(ExecutorToken.class),
"GET",
"http://somedoman/foo",
0,
JavaOnlyArray.from(invalidHeaders),
null,
true,
0);
/* requestId */ 0,
/* headers */ JavaOnlyArray.from(invalidHeaders),
/* body */ null,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
verifyErrorEmit(emitter, 0);
}
@@ -138,6 +147,9 @@ public class NetworkingModuleTest {
when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class);
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
JavaOnlyMap body = new JavaOnlyMap();
@@ -152,8 +164,9 @@ public class NetworkingModuleTest {
0,
JavaOnlyArray.of(),
body,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
verifyErrorEmit(emitter, 0);
}
@@ -196,6 +209,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
@@ -209,8 +225,9 @@ public class NetworkingModuleTest {
0,
JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")),
body,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
@@ -234,6 +251,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
@@ -248,8 +268,9 @@ public class NetworkingModuleTest {
0,
JavaOnlyArray.from(headers),
null,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture());
Headers requestHeaders = argumentCaptor.getValue().headers();
@@ -265,7 +286,8 @@ public class NetworkingModuleTest {
.thenReturn(mock(InputStream.class));
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class)))
.thenReturn(mock(RequestBody.class));
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
.thenCallRealMethod();
JavaOnlyMap body = new JavaOnlyMap();
JavaOnlyArray formData = new JavaOnlyArray();
@@ -288,6 +310,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
networkingModule.sendRequest(
@@ -297,8 +322,9 @@ public class NetworkingModuleTest {
0,
new JavaOnlyArray(),
body,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
// verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@@ -320,7 +346,8 @@ public class NetworkingModuleTest {
.thenReturn(mock(InputStream.class));
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class)))
.thenReturn(mock(RequestBody.class));
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
.thenCallRealMethod();
List<JavaOnlyArray> headers = Arrays.asList(
JavaOnlyArray.of("Accept", "text/plain"),
@@ -348,6 +375,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
networkingModule.sendRequest(
@@ -357,8 +387,9 @@ public class NetworkingModuleTest {
0,
JavaOnlyArray.from(headers),
body,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
// verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@@ -383,7 +414,8 @@ public class NetworkingModuleTest {
when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class)))
.thenReturn(inputStream);
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod();
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
.thenCallRealMethod();
when(inputStream.available()).thenReturn("imageUri".length());
final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class);
@@ -445,6 +477,9 @@ public class NetworkingModuleTest {
return callMock;
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
@@ -455,8 +490,9 @@ public class NetworkingModuleTest {
0,
JavaOnlyArray.from(headers),
body,
true,
0);
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
/* timeout */ 0);
// verify RequestBodyPart for image
PowerMockito.verifyStatic(times(1));
@@ -503,6 +539,9 @@ public class NetworkingModuleTest {
return calls[(Integer) request.tag() - 1];
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
networkingModule.initialize();
@@ -515,7 +554,8 @@ public class NetworkingModuleTest {
idx + 1,
JavaOnlyArray.of(),
null,
true,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
0);
}
verify(httpClient, times(3)).newCall(any(Request.class));
@@ -550,6 +590,9 @@ public class NetworkingModuleTest {
return calls[(Integer) request.tag() - 1];
}
});
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
when(clientBuilder.build()).thenReturn(httpClient);
when(httpClient.newBuilder()).thenReturn(clientBuilder);
NetworkingModule networkingModule =
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
@@ -561,7 +604,8 @@ public class NetworkingModuleTest {
idx + 1,
JavaOnlyArray.of(),
null,
true,
/* responseType */ "text",
/* useIncrementalUpdates*/ true,
0);
}
verify(httpClient, times(3)).newCall(any(Request.class));