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 getComics
is 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 HttpClient
can 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:
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.