Dynamic Headers per endpoint with Retrofit Frank Follow Mar 3 · 3 min read

Ready, set, go

In most of my projects, I use an OkHttp interceptor to monitors traffic and add headers to requests. There are some drawbacks to this setup.

The problem with interceptors

Fragmented query construction. The class that constructs the request (your remote repository) knows nothing about the headers that are added in the interceptor while these are part of the same request.

The class that constructs the request (your remote repository) knows nothing about the headers that are added in the interceptor while these are part of the same request. The interceptor is smart and all-knowing. Headers often differ per endpoint and may be based on app-state like login-state, auth tokens and the user-selected language. To fetch or use this logic, the interceptor needs to be too smart. It may cross module boundaries to acquire data. It can break feature-module independence.

Headers often differ per endpoint and may be based on app-state like login-state, auth tokens and the user-selected language. To fetch or use this logic, the interceptor needs to be too smart. It may cross module boundaries to acquire data. It can break feature-module independence. Overriding or manipulating issues. Header manipulation specific for one endpoint or feature is harder to achieve. Same for headers that are specific for one request, like a file upload.

Alternatives

Struggling to take off

There are ways around these problems, but they won’t make you fly. To name a few: one could add custom annotations on requests to communicate what type of headers should be set in the interceptor. This only solves a small part of the problem. Retrofit’s @Header and @Headers annotations can also be used, but often result in duplicate code.

To the rescue: Strong typed header maps

Instead of an interceptor, we can use a HeadersMap directly in the Retrofit interface. It’s short and generic but leaves room for mistakes because any map can be used as input for the request’s interface method.

// NO. (too generic and therefore room for mistakes) @GET("user")

fun getUser(@HeaderMap headers: Map<String, String>): User

However, if we make the header’s type more explicit then we won’t be able to mix up different sets of headers.

// YES @GET("user")

fun getUser(@HeaderMap authedHeaders: AuthenticatedHeaders): User

The AuthenticatedHeaders class can look like this.

/* for requests that require authentication */

class AuthenticatedHeaders : MainApiHeaders() /* for requests that are public */

class PublicHeaders : MainApiHeaders() open class MainApiHeaders: HashMap<String, String>()

Using an inline class is not desirable here because Retrofit explicitly requires a Map, and we would lose our strong narrow typed parameter if we deconstruct the inner class when calling the interface method.

Now in our Retrofit Api interface methods, we can specify explicitly what headers are needed on what request.

@GET("articles")

fun getArticles(

@HeaderMap publicHeaders: PublicHeaders): List<Articles> @GET("user")

fun getUser(@HeaderMap authedHeaders: AuthenticatedHeaders): User

In this way, trying to call getUser with the wrong headers would result in an IDE and compilation error. To provide the headers, I usually make use of a HeadersProvider per endpoint.

The HeadersProvider is injected into the feature’s Service or RemoteRepository class. The state data it needs is supplied at header-construction time. For example:

// in the class that starts the request: apiMain.getProfile(authenticatedHeaders =

headersProvider.getAuthenticatedHeaders(accessToken)

)

As a result, headers can be altered safely based on parameters like the login state or user settings. Generating different headers per endpoint is easy and the header logic can be centralized per feature, per endpoint or for the whole app, depending on the use-case.