diff --git a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java index 935f5512f..4e73f6a28 100644 --- a/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java +++ b/android/plugins/network/src/main/java/com/facebook/flipper/plugins/network/FlipperOkhttpInterceptor.java @@ -7,23 +7,37 @@ package com.facebook.flipper.plugins.network; +import android.text.TextUtils; +import android.util.Pair; +import com.facebook.flipper.core.FlipperArray; +import com.facebook.flipper.core.FlipperConnection; +import com.facebook.flipper.core.FlipperObject; +import com.facebook.flipper.core.FlipperReceiver; +import com.facebook.flipper.core.FlipperResponder; +import com.facebook.flipper.plugins.common.BufferingFlipperPlugin; import com.facebook.flipper.plugins.network.NetworkReporter.RequestInfo; import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo; import java.io.IOException; +import java.net.HttpURLConnection; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; import okhttp3.Headers; import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import okio.Buffer; import okio.BufferedSource; -public class FlipperOkhttpInterceptor implements Interceptor { +public class FlipperOkhttpInterceptor + implements Interceptor, BufferingFlipperPlugin.MockResponseConnectionListener { // By default, limit body size (request or response) reporting to 100KB to avoid OOM private static final long DEFAULT_MAX_BODY_BYTES = 100 * 1024; @@ -32,6 +46,16 @@ public class FlipperOkhttpInterceptor implements Interceptor { public @Nullable NetworkFlipperPlugin plugin; + private static class PartialRequestInfo extends Pair { + PartialRequestInfo(String url, String method) { + super(url, method); + } + } + + // pair of request url and method + private Map mMockResponseMap = new HashMap<>(0); + private boolean mIsMockResponseSupported = false; + public FlipperOkhttpInterceptor() { this.plugin = null; } @@ -46,14 +70,41 @@ public class FlipperOkhttpInterceptor implements Interceptor { this.maxBodyBytes = maxBodyBytes; } + /** + * To support mock response, addIntercept must be used (instead of addNetworkIntercept) to allow + * short circuit: https://square.github.io/okhttp/interceptors/ * + */ + public FlipperOkhttpInterceptor(NetworkFlipperPlugin plugin, boolean isMockResponseSupported) { + this.plugin = plugin; + mIsMockResponseSupported = isMockResponseSupported; + if (isMockResponseSupported) { + this.plugin.setConnectionListener(this); + } + } + + public FlipperOkhttpInterceptor( + NetworkFlipperPlugin plugin, long maxBodyBytes, boolean isMockResponseSupported) { + this.plugin = plugin; + this.maxBodyBytes = maxBodyBytes; + mIsMockResponseSupported = isMockResponseSupported; + if (isMockResponseSupported) { + this.plugin.setConnectionListener(this); + } + } + @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); String identifier = UUID.randomUUID().toString(); plugin.reportRequest(convertRequest(request, identifier)); - Response response = chain.proceed(request); + + // Check if there is a mock response + Response mockResponse = mIsMockResponseSupported ? getMockResponse(request) : null; + Response response = mockResponse != null ? mockResponse : chain.proceed(request); + ResponseBody body = response.body(); ResponseInfo responseInfo = convertResponse(response, body, identifier); + responseInfo.isMock = mockResponse != null; plugin.reportResponse(responseInfo); return response; } @@ -104,4 +155,96 @@ public class FlipperOkhttpInterceptor implements Interceptor { } return list; } + + private void registerMockResponse(PartialRequestInfo partialRequest, ResponseInfo response) { + if (!mMockResponseMap.containsKey(partialRequest)) { + mMockResponseMap.put(partialRequest, response); + } + } + + @Nullable + private Response getMockResponse(Request request) { + final String url = request.url().toString(); + final String method = request.method(); + final PartialRequestInfo partialRequest = new PartialRequestInfo(url, method); + + if (!mMockResponseMap.containsKey(partialRequest)) { + return null; + } + ResponseInfo mockResponse = mMockResponseMap.get(partialRequest); + if (mockResponse == null) { + return null; + } + + final Response.Builder builder = new Response.Builder(); + builder + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(mockResponse.statusCode) + .message(mockResponse.statusReason) + .receivedResponseAtMillis(System.currentTimeMillis()) + .body(ResponseBody.create(MediaType.parse("application/text"), mockResponse.body)); + + if (mockResponse.headers != null && !mockResponse.headers.isEmpty()) { + for (final NetworkReporter.Header header : mockResponse.headers) { + if (!TextUtils.isEmpty(header.name) && !TextUtils.isEmpty(header.value)) { + builder.header(header.name, header.value); + } + } + } + return builder.build(); + } + + @Nullable + private ResponseInfo convertFlipperObjectRouteToResponseInfo(FlipperObject route) { + final String data = route.getString("data"); + final String requestUrl = route.getString("requestUrl"); + final String method = route.getString("method"); + FlipperArray headersArray = route.getArray("headers"); + if (TextUtils.isEmpty(requestUrl) || TextUtils.isEmpty(method)) { + return null; + } + + final ResponseInfo mockResponse = new ResponseInfo(); + mockResponse.body = data.getBytes(); + mockResponse.statusCode = HttpURLConnection.HTTP_OK; + mockResponse.statusReason = "OK"; + if (headersArray != null) { + final List headers = new ArrayList<>(); + for (int j = 0; j < headersArray.length(); j++) { + final FlipperObject header = headersArray.getObject(j); + headers.add(new NetworkReporter.Header(header.getString("key"), header.getString("value"))); + } + mockResponse.headers = headers; + } + return mockResponse; + } + + @Override + public void onConnect(FlipperConnection connection) { + connection.receive( + "mockResponses", + new FlipperReceiver() { + @Override + public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception { + FlipperArray array = params.getArray("routes"); + mMockResponseMap.clear(); + for (int i = 0; i < array.length(); i++) { + final FlipperObject route = array.getObject(i); + final String requestUrl = route.getString("requestUrl"); + final String method = route.getString("method"); + ResponseInfo mockResponse = convertFlipperObjectRouteToResponseInfo(route); + if (mockResponse != null) { + registerMockResponse(new PartialRequestInfo(requestUrl, method), mockResponse); + } + } + responder.success(); + } + }); + } + + @Override + public void onDisconnect() { + mMockResponseMap.clear(); + } } diff --git a/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java b/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java index a65056996..d7806c28a 100644 --- a/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java +++ b/android/sample/src/debug/java/com/facebook/flipper/sample/FlipperInitializer.java @@ -36,7 +36,7 @@ public final class FlipperInitializer { final DescriptorMapping descriptorMapping = DescriptorMapping.withDefaults(); final NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin(); - final FlipperOkhttpInterceptor interceptor = new FlipperOkhttpInterceptor(networkPlugin); + final FlipperOkhttpInterceptor interceptor = new FlipperOkhttpInterceptor(networkPlugin, true); // Normally, you would want to make this dependent on a BuildConfig flag, but // for this demo application we can safely assume that you always want to debug. @@ -60,7 +60,7 @@ public final class FlipperInitializer { final OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addNetworkInterceptor(interceptor) + .addInterceptor(interceptor) .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.MINUTES)