Request timed out! But you didn’t expect it, did you? Of course not because while you were writing the app and testing the code you were always on your blazing fast WiFi connection or a 4G LTE network. But in the real world, all your users don’t have access to such a network connection at all times.
Would you want them to suffer?
Would you want your app to behave in a strange manner in that case?
Would you want to create a bad user experience?
Would you want to have an unsatisfied customer?
Do you always test all your features depended on network requests for flaky connections?
If your answer to all the above questions is “No”, then I have a simple solution for you, that’ll make your life a lot easier to test your features for these scenarios and have a failure mechanism in place to be more responsive to the user.
Implementation
The solution is pretty straight forward, create an Interceptor for your network requests and delay or fail them. Let’s look at how to implement this interceptor with OkHttp and provide easy access to it through a UI to all the stakeholders that are responsible or are willing to test your application.
public class NetworkThrottlingInterceptor implements Interceptor { private static boolean failRequests; private static final AtomicLong failRequestCount = new AtomicLong(Long.MAX_VALUE); private static boolean delayAllRequests; private static long minRequestDelay; private static long maxRequestDelay; private final Random random = new Random(4); @Override public Response intercept(Chain chain) throws IOException { if(failRequests) { long failC = failRequestCount.get(); if (failC > 0) {failRequestCount.compareAndSet(failC, failC-1); throw new IOException("FAIL ALL REQUESTS"); } } if(delayAllRequests) { long delay = minRequestDelay; if(minRequestDelay != maxRequestDelay) { delay = (long) ((random.nextDouble() * (maxRequestDelay - minRequestDelay)) + minRequestDelay); } long end = System.currentTimeMillis() + delay; long now = System.currentTimeMillis(); while(now < end) { try { Thread.sleep(end - now); } catch (InterruptedException e) { // Nothing to do here, timing controlled by outer loop. } now = System.currentTimeMillis(); } } try { return chain.proceed(chain.request()); } catch (Exception ex) { if (BuildConfig.DEBUG) { Request request = chain.request(); Log.e("NETWORK", "Exception during request", ex); Log.e("NETWORK", "Request was to: " + request.url().toString()); } throw ex; } }public static void delayAllRequests(long minRequestDelay, long maxRequestDelay) { if(minRequestDelay == 0 && maxRequestDelay == 0) {delayAllRequests = false; } else { NetworkThrottlingInterceptor.minRequestDelay = minRequestDelay; NetworkThrottlingInterceptor.maxRequestDelay = maxRequestDelay;delayAllRequests = true; } }public static void failNextRequests(long failCount) { if(failCount == 0) {failRequests = false; } else {failRequestCount.set(failCount);failRequests = true; } }}
The logic in the interceptor is quite simple, but for verbosity, I’ll still explain it here:
We have two scenarios that we deal with, through this interceptor:
- Delay a response for a given network request.
- Fail next n network requests.
Scenario 1: Delay a response for a given network request
For this scenario, we set a minimum and maximum value and for a given request we find a random value between this range and ask the thread to sleep for that time.
You can create a different combination of settings that you give access to, through your UI, for example:
- Good Network (min = 0, max = 0)
- Slow Network (min = 1 second, max =5 seconds)
- Very Slow Network (min = 5 seconds, max = 10 seconds)
By default, your app can always be set to work on the Good Network. And then the users can switch to other networks as and when needed.
Scenario 2: Cause next n network requests to fail
For this scenario, we maintain a fail counter that keeps decreasing on each request until it becomes zero. And while this happens we just throw an IO Exception to cause the network request to fail, you can even cause a failure by other means like creating a fake failure response with some status code 5xx.
Interceptor Integration
Injection of this interceptor is quite simple you can add it at the time of configuration of your OkHttp client instance like so.
OkHttpClient okHttpClient = new OkHttpClient.Builder() .addNetworkInterceptor(new NetworkThrottlingInterceptor())
.build();
Interceptor Exposure
In our app, we do it through a debug screen that contains a simple Spinner widget that holds all these values that we can select and modify the interceptor configuration at runtime, the code for which looks like this
private void setupNetworkThrottler() { spNetworkInterceptor = findViewById(R.id.sp_network_throttle); ArrayAdapter<String> networkSpeedTypeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, new String[]{"Good network", "Moderate network 1-5s delay", "Poor network 5-10s delay"}); spNetworkInterceptor.setAdapter(networkSpeedTypeAdapter); spNetworkInterceptor.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { switch (position) { case 0: NetworkThrottlingInterceptor.delayAllRequests(0,0); break; case 1: NetworkThrottlingInterceptor.delayAllRequests(1000, 5000); break; case 2: NetworkThrottlingInterceptor.delayAllRequests(5000, 10000); break; } } @Override public void onNothingSelected(AdapterView<?> parent) { NetworkThrottlingInterceptor.delayAllRequests(0,0); } });}
If you have other interesting solutions that you can leverage this interceptor for, please share them with me on Twitter where you can find me as @droidchef.