Hocus pocus: painless headers customization of REST API requests in Flutter
Below youβll discover how to provide different headers to individual, selected, most, or all REST API requests without explicitly passing them as parameters when using retrofit code generating package or alike. The solution is based on Dart annotations and Dio interceptors.
In frontend applications, interaction with the backend is one of the more or less established and standardized areas. Most apps consume RESTful APIs, exchanging data in JSON format, and appending some configuration in headers.
Different tools for Dart and Flutter applications facilitate the implementation of a layer of communication with the backend. Here are two of them in particular.
Dio
dio package offers a powerful HTTP client implementation:
Here is a usage example. ExampleApi
accepts dio
object as a parameter and performs GET and POST requests with the configuration necessary:
Given that token
can be asynchronously obtained from some AuthService
:
the ExampleApi
methods can be used like:
Retrofit
retrofit is a code-generating package that implements REST API requests to a Dio
instance based on a configuration:
A code example below reimplements the dio
usage example from above more efficiently. For example_api.dart
:
after the code generation is done via:
the generated example_api.g.dart
file contains:
The generated code is similar to the manual implementation, requiring fewer developer efforts.
The usage of ExampleApi
did not have to change at all:
In the example above, where headers were provided to individual requests, they were either a static map @Headers({'api-key': 'api-key-value'})
or passed explicitly as a parameter @Header('Authorization') String token
. In real-life applications, which typically have more than two API calls, this means @Headers({'api-key': 'api-key-value'})
has to be repeated over and over again, and callers of requests requiring authentication, like /users
, have to provide token
again and again. This leads to code duplications, unneeded dependencies, mistakes in headersβ names and values, and a risk of forgetting the required header here and there.
There are multiple improvements to the straightforward implementation shown above. Let me show you some tricks!
Hocus pocus #1: Dart annotations
Dart allows using any const
value as an annotation. It can be a primitive type like String
:
or custom type:
This knowledge can help slightly improve the code duplication in header names and values situation. New const
values based on Headers
and Header
classes from retrofit package:
can be applied as annotations to requests in ExampleApi
:
removing the need to repeat Authorization
header name or api-key
header name and value. The code generated to example_api.g.dart
for this version of ExampleApi
is completely identical to the first implementation.
Better, but still, there is a need to remember to apply the @authorizedRequest
annotation to all requests. So letβs see how the implementation can be further improved.
Hocus pocus #2: Dio interceptors
Dio
interceptor is a class that can be attached to a Dio
instance, and can intercept requests, responses, and errors before they are handled by the userβs code. For example:
When such an interceptor is attached to the Dio
instance:
the onRequest
method has full power to modify any options, headers, and data of the request on its way from ExampleApi.getUser()
to Dio.fetch()
call.
If the requirement is to append a header to all API requests, the answer is straightforward: attach a Dio
interceptor that would always add the header. The value can be static or can be configured when the interceptor is created, as in AppendApiKeyInterceptor
.
Now the @authorizedRequest
annotation can be removed from the ExampleApi
completely, but still, requests will be authorized with provided api-key
header:
The repeated need to provide Authorization
header, however, remains. It is required for the /users
and similar authenticated requests, but not for /forgot-password.
If a header is required for most API requests with only a few exceptions (for example, Authorization
is not required for the /login
and /forgot-password
requests because the user is not yet logged in), the dedicated interceptor can keep a list of such exceptions and append the header to all the rest of the requests:
Now ExampleApi
requests wonβt need to accept the Authorization
header value either:
But still, after AppendTokenInterceptor
is attached to a Dio
instance:
all requests will be authenticated with the Authorization
header.
The resulting AppendApiKeyInterceptor
does not depend on any condition and appends the api-key
header to all requests. The AppendTokenInterceptor
responsible for appending the Authorization
header to most of the requests depends on the list of exceptions implemented inside the interceptor. While maintaining a relatively short list of exceptions is an acceptable solution for small to medium projects, this is not feasible for big projects with hundreds of API calls.
In that case, information about the fact that the selected request needs a certain header should remain in the place of the request implementation. In our case β in ExampleApi
. However, instead of providing the exact headersβ names and values, a task is to somehow mark API requests with the set of headers they require, read that mark in a Dio
interceptor, and decide whether to append the header value. Here is a trick on how to do it!
Hocus pocus #3: hocus pocus #1 + hocus pocus #2
We already know that annotations are a great way to mark Dart code. To solve the problem above, we need a special annotation that will have an effect on the code generated by retrofit and provide marking data that can later be read in the Dio
interceptor β @Extra
. For this example_api.dart
:
the generated example_api.g.dart
file contains:
The extra
parameter allows carrying additional Map
of data from the request call to the Dio
interceptor, where it can be used to make a decision on whether to append a header value:
The usage of ExampleApi
and AppendTokenInterceptor
did not change since the last example:
The Extra
object can be saved to a const
value:
and applied as annotation above requests which require the Authorization
header :
The generated code in example_api.g.dart
file does not change.
The Headers
class also can be used as a base for @authenticatedRequest
annotation. However, the usage of Extra
class is preferred over Headers
to keep us safe in case no Dio
interceptor is attached. Content of Extra
map is not sent to the backend, and itβs not a big deal if no interceptor took care of deleting append-token
record. Itβs different with Headers
map, though β its content will be sent to the backend, and having unexpected data there may cause request rejection.
There is, however, an important limitation to keep in mind: only one Extra
can be provided above retrofit requests. Why does it matter?
Letβs say there is another header required for certain requests: Accept-Language
. It would seem logical that a new annotation could be created:
and a dedicated Dio
interceptor, awaiting for append-language
record would append Accept-Language
header when it meets one:
However, itβs impossible to apply both @authenticatedRequest
and @localizedRequest
annotations over the same retrofit request, the generated code will contain Extra
only from the first used annotation. For this example_api.dart
:
the generated example_api.g.dart
file contains:
which means only a /forgot-password
request would have Accept-Language
header appended. To fix this, the content of authenticatedRequest
and localizedRequest
maps has to be merged into a new const
value:
which then can be applied over a retrofit request. For this version of example_api.dart
:
the generated example_api.g.dart
file contains:
which means now both /users
and /forgot-password
will have Accept-Language
header appended, and /users
will also have Authorization
header.
<update>
After my beloved husband, Oleksandr Leushchenko read the draft of this article, he could not resist fixing the limitation on the number of @Extra
and @Headers
attributes allowed above retrofit requests. As a result, the new version of retrofit_generator: ^4.0.3
allows the creation of code like this:
and the generated example_api.g.dart
file will be completely identical to the one above.
Still having multi-value Extra
maps like authenticatedLocalizedRequest
may be beneficial to keep the code DRY.
</update>
With this approach, each request is marked with the set of headers it requires without explicitly dealing with the exact values. And Dio
interceptors don't need to manage internal black/white lists of requests in order to know whether to append headers.
Whether you think such a thing does not exist, or you do believe in magic, or even practice it on a daily basis, let me know if you liked the hocus pocus I showed you.
By the way, I talk about some more cool networking-related stuff in a series of articles: Basic and advanced networking in Dart and Flutter β the Tide way.