WebWorkers: Convert NetworkingModule to support web workers

Summary: Converts NetworkingModule to dispatch callbacks to the appropriate ExecutionContext.

Reviewed By: lexs

Differential Revision: D2932170

fb-gh-sync-id: ac77eec1a176ede4d2257bfd8ddb13153331c8a4
shipit-source-id: ac77eec1a176ede4d2257bfd8ddb13153331c8a4
This commit is contained in:
Andy Street 2016-03-03 11:06:45 -08:00 committed by Facebook Github Bot 9
parent 532e41105b
commit 9a3f11d3e7
3 changed files with 143 additions and 97 deletions

View File

@ -12,10 +12,10 @@ package com.facebook.react.modules.core;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.SupportsWebWorkers;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
/** /**
@ -23,7 +23,8 @@ import com.facebook.react.bridge.UiThreadUtil;
*/ */
public class DeviceEventManagerModule extends ReactContextBaseJavaModule { public class DeviceEventManagerModule extends ReactContextBaseJavaModule {
public static interface RCTDeviceEventEmitter extends JavaScriptModule { @SupportsWebWorkers
public interface RCTDeviceEventEmitter extends JavaScriptModule {
void emit(String eventName, @Nullable Object data); void emit(String eventName, @Nullable Object data);
} }

View File

@ -18,6 +18,7 @@ import java.io.Reader;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ExecutorToken;
import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
@ -116,9 +117,10 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
@ReactMethod @ReactMethod
/** /**
* @param timeout value of 0 results in no timeout * @param timeout value of 0 results in no timeout
*/ */
public void sendRequest( public void sendRequest(
final ExecutorToken executorToken,
String method, String method,
String url, String url,
final int requestId, final int requestId,
@ -144,7 +146,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
Headers requestHeaders = extractHeaders(headers, data); Headers requestHeaders = extractHeaders(headers, data);
if (requestHeaders == null) { if (requestHeaders == null) {
onRequestError(requestId, "Unrecognized headers format"); onRequestError(executorToken, requestId, "Unrecognized headers format");
return; return;
} }
String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME); String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME);
@ -155,7 +157,10 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method)); requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) { } else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
if (contentType == null) { if (contentType == null) {
onRequestError(requestId, "Payload is set but no content-type header specified"); onRequestError(
executorToken,
requestId,
"Payload is set but no content-type header specified");
return; return;
} }
String body = data.getString(REQUEST_BODY_KEY_STRING); String body = data.getString(REQUEST_BODY_KEY_STRING);
@ -163,7 +168,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
if (RequestBodyUtil.isGzipEncoding(contentEncoding)) { if (RequestBodyUtil.isGzipEncoding(contentEncoding)) {
RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body); RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
if (requestBody == null) { if (requestBody == null) {
onRequestError(requestId, "Failed to gzip request body"); onRequestError(executorToken, requestId, "Failed to gzip request body");
return; return;
} }
requestBuilder.method(method, requestBody); requestBuilder.method(method, requestBody);
@ -172,14 +177,17 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} }
} else if (data.hasKey(REQUEST_BODY_KEY_URI)) { } else if (data.hasKey(REQUEST_BODY_KEY_URI)) {
if (contentType == null) { if (contentType == null) {
onRequestError(requestId, "Payload is set but no content-type header specified"); onRequestError(
executorToken,
requestId,
"Payload is set but no content-type header specified");
return; return;
} }
String uri = data.getString(REQUEST_BODY_KEY_URI); String uri = data.getString(REQUEST_BODY_KEY_URI);
InputStream fileInputStream = InputStream fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri); RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri);
if (fileInputStream == null) { if (fileInputStream == null) {
onRequestError(requestId, "Could not retrieve file for uri " + uri); onRequestError(executorToken, requestId, "Could not retrieve file for uri " + uri);
return; return;
} }
requestBuilder.method( requestBuilder.method(
@ -190,7 +198,8 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
contentType = "multipart/form-data"; contentType = "multipart/form-data";
} }
ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA); ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA);
MultipartBuilder multipartBuilder = constructMultipartBody(parts, contentType, requestId); MultipartBuilder multipartBuilder =
constructMultipartBody(executorToken, parts, contentType, requestId);
if (multipartBuilder == null) { if (multipartBuilder == null) {
return; return;
} }
@ -207,7 +216,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
if (mShuttingDown) { if (mShuttingDown) {
return; return;
} }
onRequestError(requestId, e.getMessage()); onRequestError(executorToken, requestId, e.getMessage());
} }
@Override @Override
@ -217,25 +226,28 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} }
// Before we touch the body send headers to JS // Before we touch the body send headers to JS
onResponseReceived(requestId, response); onResponseReceived(executorToken, requestId, response);
ResponseBody responseBody = response.body(); ResponseBody responseBody = response.body();
try { try {
if (useIncrementalUpdates) { if (useIncrementalUpdates) {
readWithProgress(requestId, responseBody); readWithProgress(executorToken, requestId, responseBody);
onRequestSuccess(requestId); onRequestSuccess(executorToken, requestId);
} else { } else {
onDataReceived(requestId, responseBody.string()); onDataReceived(executorToken, requestId, responseBody.string());
onRequestSuccess(requestId); onRequestSuccess(executorToken, requestId);
} }
} catch (IOException e) { } catch (IOException e) {
onRequestError(requestId, e.getMessage()); onRequestError(executorToken, requestId, e.getMessage());
} }
} }
}); });
} }
private void readWithProgress(int requestId, ResponseBody responseBody) throws IOException { private void readWithProgress(
ExecutorToken executorToken,
int requestId,
ResponseBody responseBody) throws IOException {
Reader reader = responseBody.charStream(); Reader reader = responseBody.charStream();
try { try {
StringBuilder sb = new StringBuilder(getBufferSize(responseBody)); StringBuilder sb = new StringBuilder(getBufferSize(responseBody));
@ -246,14 +258,14 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
sb.append(buffer, 0, read); sb.append(buffer, 0, read);
long now = System.nanoTime(); long now = System.nanoTime();
if (shouldDispatch(now, last)) { if (shouldDispatch(now, last)) {
onDataReceived(requestId, sb.toString()); onDataReceived(executorToken, requestId, sb.toString());
sb.setLength(0); sb.setLength(0);
last = now; last = now;
} }
} }
if (sb.length() > 0) { if (sb.length() > 0) {
onDataReceived(requestId, sb.toString()); onDataReceived(executorToken, requestId, sb.toString());
} }
} finally { } finally {
reader.close(); reader.close();
@ -273,31 +285,34 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} }
} }
private void onDataReceived(int requestId, String data) { private void onDataReceived(ExecutorToken ExecutorToken, int requestId, String data) {
WritableArray args = Arguments.createArray(); WritableArray args = Arguments.createArray();
args.pushInt(requestId); args.pushInt(requestId);
args.pushString(data); args.pushString(data);
getEventEmitter().emit("didReceiveNetworkData", args); getEventEmitter(ExecutorToken).emit("didReceiveNetworkData", args);
} }
private void onRequestError(int requestId, String error) { private void onRequestError(ExecutorToken ExecutorToken, int requestId, String error) {
WritableArray args = Arguments.createArray(); WritableArray args = Arguments.createArray();
args.pushInt(requestId); args.pushInt(requestId);
args.pushString(error); args.pushString(error);
getEventEmitter().emit("didCompleteNetworkResponse", args); getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args);
} }
private void onRequestSuccess(int requestId) { private void onRequestSuccess(ExecutorToken ExecutorToken, int requestId) {
WritableArray args = Arguments.createArray(); WritableArray args = Arguments.createArray();
args.pushInt(requestId); args.pushInt(requestId);
args.pushNull(); args.pushNull();
getEventEmitter().emit("didCompleteNetworkResponse", args); getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args);
} }
private void onResponseReceived(int requestId, Response response) { private void onResponseReceived(
ExecutorToken ExecutorToken,
int requestId,
Response response) {
WritableMap headers = translateHeaders(response.headers()); WritableMap headers = translateHeaders(response.headers());
WritableArray args = Arguments.createArray(); WritableArray args = Arguments.createArray();
@ -306,7 +321,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
args.pushMap(headers); args.pushMap(headers);
args.pushString(response.request().urlString()); args.pushString(response.request().urlString());
getEventEmitter().emit("didReceiveNetworkResponse", args); getEventEmitter(ExecutorToken).emit("didReceiveNetworkResponse", args);
} }
private static WritableMap translateHeaders(Headers headers) { private static WritableMap translateHeaders(Headers headers) {
@ -326,7 +341,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} }
@ReactMethod @ReactMethod
public void abortRequest(final int requestId) { public void abortRequest(ExecutorToken executorToken, final int requestId) {
// We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an // We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an
// open issue on OkHttp: https://github.com/square/okhttp/issues/869 // open issue on OkHttp: https://github.com/square/okhttp/issues/869
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) { new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@ -338,11 +353,21 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
} }
@ReactMethod @ReactMethod
public void clearCookies(com.facebook.react.bridge.Callback callback) { public void clearCookies(
ExecutorToken executorToken,
com.facebook.react.bridge.Callback callback) {
mCookieHandler.clearCookies(callback); mCookieHandler.clearCookies(callback);
} }
private @Nullable MultipartBuilder constructMultipartBody( @Override
public boolean supportsWebWorkers() {
return true;
}
private
@Nullable
MultipartBuilder constructMultipartBody(
ExecutorToken ExecutorToken,
ReadableArray body, ReadableArray body,
String contentType, String contentType,
int requestId) { int requestId) {
@ -356,7 +381,10 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
ReadableArray headersArray = bodyPart.getArray("headers"); ReadableArray headersArray = bodyPart.getArray("headers");
Headers headers = extractHeaders(headersArray, null); Headers headers = extractHeaders(headersArray, null);
if (headers == null) { if (headers == null) {
onRequestError(requestId, "Missing or invalid header format for FormData part."); onRequestError(
ExecutorToken,
requestId,
"Missing or invalid header format for FormData part.");
return null; return null;
} }
MediaType partContentType = null; MediaType partContentType = null;
@ -373,19 +401,25 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue)); multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue));
} else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) { } else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) {
if (partContentType == null) { if (partContentType == null) {
onRequestError(requestId, "Binary FormData part needs a content-type header."); onRequestError(
ExecutorToken,
requestId,
"Binary FormData part needs a content-type header.");
return null; return null;
} }
String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI); String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI);
InputStream fileInputStream = InputStream fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr); RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr);
if (fileInputStream == null) { if (fileInputStream == null) {
onRequestError(requestId, "Could not retrieve file for uri " + fileContentUriStr); onRequestError(
ExecutorToken,
requestId,
"Could not retrieve file for uri " + fileContentUriStr);
return null; return null;
} }
multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream)); multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream));
} else { } else {
onRequestError(requestId, "Unrecognized FormData part."); onRequestError(ExecutorToken, requestId, "Unrecognized FormData part.");
} }
} }
return multipartBuilder; return multipartBuilder;
@ -394,7 +428,9 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
/** /**
* Extracts the headers from the Array. If the format is invalid, this method will return null. * Extracts the headers from the Array. If the format is invalid, this method will return null.
*/ */
private @Nullable Headers extractHeaders( private
@Nullable
Headers extractHeaders(
@Nullable ReadableArray headersArray, @Nullable ReadableArray headersArray,
@Nullable ReadableMap requestData) { @Nullable ReadableMap requestData) {
if (headersArray == null) { if (headersArray == null) {
@ -423,8 +459,8 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
return headersBuilder.build(); return headersBuilder.build();
} }
private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter() { private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter(ExecutorToken ExecutorToken) {
return getReactApplicationContext() return getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); .getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class);
} }
} }

View File

@ -14,6 +14,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ExecutorToken;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.JavaOnlyArray;
@ -88,13 +89,14 @@ public class NetworkingModuleTest {
NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient);
networkingModule.sendRequest( networkingModule.sendRequest(
"GET", mock(ExecutorToken.class),
"http://somedomain/foo", "GET",
0, "http://somedomain/foo",
JavaOnlyArray.of(), 0,
null, JavaOnlyArray.of(),
true, null,
0); true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class); ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture()); verify(httpClient).newCall(argumentCaptor.capture());
@ -108,7 +110,7 @@ public class NetworkingModuleTest {
public void testFailGetWithInvalidHeadersStruct() throws Exception { public void testFailGetWithInvalidHeadersStruct() throws Exception {
RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class); RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class);
ReactApplicationContext context = mock(ReactApplicationContext.class); ReactApplicationContext context = mock(ReactApplicationContext.class);
when(context.getJSModule(any(Class.class))).thenReturn(emitter); when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class); OkHttpClient httpClient = mock(OkHttpClient.class);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
@ -118,13 +120,14 @@ public class NetworkingModuleTest {
mockEvents(); mockEvents();
networkingModule.sendRequest( networkingModule.sendRequest(
"GET", mock(ExecutorToken.class),
"http://somedoman/foo", "GET",
0, "http://somedoman/foo",
JavaOnlyArray.from(invalidHeaders), 0,
null, JavaOnlyArray.from(invalidHeaders),
true, null,
0); true,
0);
verifyErrorEmit(emitter, 0); verifyErrorEmit(emitter, 0);
} }
@ -133,7 +136,7 @@ public class NetworkingModuleTest {
public void testFailPostWithoutContentType() throws Exception { public void testFailPostWithoutContentType() throws Exception {
RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class); RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class);
ReactApplicationContext context = mock(ReactApplicationContext.class); ReactApplicationContext context = mock(ReactApplicationContext.class);
when(context.getJSModule(any(Class.class))).thenReturn(emitter); when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
OkHttpClient httpClient = mock(OkHttpClient.class); OkHttpClient httpClient = mock(OkHttpClient.class);
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
@ -144,13 +147,14 @@ public class NetworkingModuleTest {
mockEvents(); mockEvents();
networkingModule.sendRequest( networkingModule.sendRequest(
"POST", mock(ExecutorToken.class),
"http://somedomain/bar", "POST",
0, "http://somedomain/bar",
JavaOnlyArray.of(), 0,
body, JavaOnlyArray.of(),
true, body,
0); true,
0);
verifyErrorEmit(emitter, 0); verifyErrorEmit(emitter, 0);
} }
@ -184,7 +188,7 @@ public class NetworkingModuleTest {
} }
@Test @Test
public void testSuccessfullPostRequest() throws Exception { public void testSuccessfulPostRequest() throws Exception {
OkHttpClient httpClient = mock(OkHttpClient.class); OkHttpClient httpClient = mock(OkHttpClient.class);
when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer<Object>() { when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer<Object>() {
@Override @Override
@ -200,13 +204,14 @@ public class NetworkingModuleTest {
body.putString("string", "This is request body"); body.putString("string", "This is request body");
networkingModule.sendRequest( networkingModule.sendRequest(
"POST", mock(ExecutorToken.class),
"http://somedomain/bar", "POST",
0, "http://somedomain/bar",
JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")), 0,
body, JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")),
true, body,
0); true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class); ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture()); verify(httpClient).newCall(argumentCaptor.capture());
@ -237,13 +242,14 @@ public class NetworkingModuleTest {
JavaOnlyArray.of("User-Agent", "React test agent/1.0")); JavaOnlyArray.of("User-Agent", "React test agent/1.0"));
networkingModule.sendRequest( networkingModule.sendRequest(
"GET", mock(ExecutorToken.class),
"http://someurl/baz", "GET",
0, "http://someurl/baz",
JavaOnlyArray.from(headers), 0,
null, JavaOnlyArray.from(headers),
true, null,
0); true,
0);
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class); ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
verify(httpClient).newCall(argumentCaptor.capture()); verify(httpClient).newCall(argumentCaptor.capture());
Headers requestHeaders = argumentCaptor.getValue().headers(); Headers requestHeaders = argumentCaptor.getValue().headers();
@ -284,13 +290,14 @@ public class NetworkingModuleTest {
NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient);
networkingModule.sendRequest( networkingModule.sendRequest(
"POST", mock(ExecutorToken.class),
"http://someurl/uploadFoo", "POST",
0, "http://someurl/uploadFoo",
new JavaOnlyArray(), 0,
body, new JavaOnlyArray(),
true, body,
0); true,
0);
// verify url, method, headers // verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class); ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@ -342,13 +349,14 @@ public class NetworkingModuleTest {
NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient);
networkingModule.sendRequest( networkingModule.sendRequest(
"POST", mock(ExecutorToken.class),
"http://someurl/uploadFoo", "POST",
0, "http://someurl/uploadFoo",
JavaOnlyArray.from(headers), 0,
body, JavaOnlyArray.from(headers),
true, body,
0); true,
0);
// verify url, method, headers // verify url, method, headers
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class); ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
@ -437,13 +445,14 @@ public class NetworkingModuleTest {
NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient);
networkingModule.sendRequest( networkingModule.sendRequest(
"POST", mock(ExecutorToken.class),
"http://someurl/uploadFoo", "POST",
0, "http://someurl/uploadFoo",
JavaOnlyArray.from(headers), 0,
body, JavaOnlyArray.from(headers),
true, body,
0); true,
0);
// verify RequestBodyPart for image // verify RequestBodyPart for image
PowerMockito.verifyStatic(times(1)); PowerMockito.verifyStatic(times(1));