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.

--

--

Anna Leushchenko πŸ‘©β€πŸ’»πŸ’™πŸ“±πŸ‡ΊπŸ‡¦
Flutter Community

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