Basic and advanced networking in Dart and Flutter — the Tide way. Part 4: HTTP client and request interceptors with dio. Advanced.

Feeling lost? Check out the introduction into this series.

Parts 3 and 4 of this series are dedicated to setting up an HTTP client used to load data from the backend.

This part aims to show a more advanced dio object setup.

In this part:

1. mocked API
2. proxy
3. SSL pinning

For basic topics check out Part 3. It talks about:

1. dio HTTP client
2. dio interceptors
3. first API request

0. Prerequisites

Further code examples are built on top of the code developed in Part 3, which can be found under the part-3 tag in the Flutter Advanced Networking GitHub repository.

1. Mocked API

Apart from what was described previously in Part 3, section 2, at Tide we came up with another interesting application of dio interceptors concept.

In many cases, we are creating sociable unit tests, which means we are testing more than one layer of abstraction at a time. The testing strategy at Tide is an interesting topic on its own and can be a subject for the next series. But to give you a quick glance, here is an example. Take a look at this repository:

It injects MarvelComicsApi dependency into a constructor. When getComicsis called, it asks _api to load and parse the response, and return the data.

With the solitary tests, a MarvelComicsApi mock or stub would be created and set up to return a mocked MarvelApiResponse<MarvelPaginatedList<MarvelComic>>> object. And the MarvelComicsRepository would be tested to call the getComics method of the MarvelComicsApi and return the proper data.

Such a test would only be testing the interaction between MarvelComicsRepository and MarvelComicsApi, and tests for parsing MarvelComic, MarvelPaginatedList, and MarvelApiResponse models, and for interaction between MarvelComicsApi and dio would yet to be written.

Instead, we test them all together. We only mock the dio response with the help of an interceptor. If a request path is /comics, it’ll return a prepared marvelComicsApiGetComicsResponseString, which is a complete copy of the real response from the Marvel Comic API. Otherwise, it’ll let perform the real request, or actually fail in tests:

Now the sociable test is able to test the interaction between MarvelComicsRepository, MarvelComicsApi, and dio, and parsing of MarvelComic, MarvelPaginatedList, and MarvelApiResponse models:

marvelComicsApiGetComicsResponseData is a MarvelPaginatedList<MarvelComic> object, containing data corresponding to the content of marvelComicsApiGetComicsResponseString.

To be completely honest, we don’t even write unit tests for repositories, but for the entire application features. As a result, we don’t create objects like marvelComicsApiGetComicsResponseData, but instead, we use widget tests to check what users are supposed to see on the screen. We also do not have an individual dio interceptor for every path, but have a universal configurable interceptor for mocking backend responses in tests. As I said, our testing strategy is a topic for another series.

There is an interesting side effect of this approach. Sometimes it may happen that the mobile application is being developed faster than the required APIs. Or the backend dev environment may be down for maintenance. Or even trickier, we may have to develop an integration with a third-party service, that is either being developed or can also be down for some reason, over which we have no control. To unblock ourselves and keep developing the code as close to the production version as possible, we can use such dio interceptors outside of tests in order to mock the real API responses:

When initialized like this, dio will not perform a real call to the Marvel Comic API to get the comics list, but instead, will return a predefined response. This is a very flexible way to mock the backend behavior, including error scenarios.

2. Proxy

There are multiple reasons the application should respect device proxy configuration. Users might have a security setup configured on their devices that prevents the application from using the internet at all if it does not respect proxy settings. The testing team might want to test the application against a mocked backend, etc. By default, the device proxy settings are ignored by the application. Here is how we fix that.

Internally, dio uses an HttpClient to perform requests through the adapter. dio implements standard and developer-friendly API, and HttpClient is the real object that makes HTTP requests. The DefaultHttpClientAdapter default adapter exposes a onHttpClientCreate callback providing a chance to interact with the inner HttpClient object. It exposes a findProxy field that is a callback that reads the proxy settings:

The _findProxy function will be called with each request to dio object, and it is where the inner HttpClientcan be informed of the current proxy setup. Currently, it returns DIRECT value which means no proxy setup. How to read the real device proxy settings?

At Tide we use a custom-made plugin to read device proxy settings, which includes communicating with the native side through MethodChannel. However, for the simplicity of this example, native_flutter_proxy plugin is used:

The readProxySetting method asynchronously reads proxy settings and returns the latest values or null:

Reading device proxy settings is an asynchronous operation that returns a Future. Which makes it impossible to use this method directly in findProxy callback of the HttpClient object. To overcome this limitation we developed a three parts solution:

  1. ProxyHolder is a shared in-memory storage for proxy settings:

2. The dio interceptor ProxyInterceptor asynchronously reads device proxy settings with readProxySetting on each request, and stores the result in ProxyHolder:

3. ProxyFinder exposes findProxy method, which then synchronously reads proxy settings from ProxyHolder:

Both ProxyFinder and ProxyInterceptor share the same instance of ProxyHolder.

It’s all put together when a dio instance is created:

As a result, the onRequest method of ProxyInterceptor is called before each API call and saves current device proxy settings in the ProxyHolder. The findProxy callback of ProxyFinder is also called before each API call, but after interceptor, so it can be sure to read fresh proxy settings from shared ProxyHolder.

If there is no requirement to immediately operate with new proxy settings, ProxyHolder could return cached values until the app is restarted.

3. SSL pinning

Now, if the device has the proxy configuration set up to point to the tools like Charles Proxy, requests will get logged, but there will be no human-readable content or possibility to modify it. That’s the beauty of HTTPS.

However, if there is an attempt to read the traffic with SSL Proxying, the application will throw HandshakeException, meaning the SSL handshake failed. That’s because the Flutter application does not trust the self-signed SSL certificates web debugging proxy applications use. If there is a need to allow reading encrypted traffic through such tools, their certificates should be whitelisted in the application.

For that, upload the SSL proxying tool root certificate in PEM format under theassets directory of the Flutter app:

And add it to the set of trusted certificates before making the first API call:

Now it should be possible to intercept encrypted HTTS traffic in a web debugging proxy application like Charles Proxy.

However, in reality, for an extra layer of security, the only certificate an application should trust is the one from the server it is going to communicate to, and reject communicating with all others. That is called SSL pinning.

Because Marvel Comic API used in this series is a public API, it was possible to download its certificate with:

And convert to PEM format with:

To pin this certificate, the process is the same — add it to the application assets, load it at runtime, and add it to the set of trusted certificates with setTrustedCertificatesBytes method. To avoid the asynchronous process of reading from assets on the app start, we actually have read the content of the certificate once and saved the Uint8List version to a compile-time constant:

Now, when making a request to Marvel Comic API, the application will only communicate with the server with this public certificate.

Conclusion

We now have applied a more advanced setup to the dio instance used to perform API requests.

The final version of the code developed in this part is located under the part-4 tag in the Flutter Advanced Networking GitHub repository.

Read on Part 5: REST API requests with retrofit. Basic.

--

--

Anna Leushchenko 👩‍💻💙📱🇺🇦
Tide Engineering Team

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator