A few days ago, I ran into an unexpected problem with Retrofit. Retrofit is a nice piece of library that lets you declare your HTTP API in the form of an interface and generates the code to makes the HTTP calls for you. An example from the documentation:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Call<List<Repo>> repos = service.listRepos("octocat");

That very nice and I wanted to start using it to replace my hand-made code for querying my HTTP API on FreshRSS Android.

But, as I said, I ran into a problem: I wanted to use custom annotations to tweak the behavior of some of my methods (dd HTTP headers or refreshing the authorization token). Digging into the documentation, I found it was possible to write call adapters to modify the way Retrofit performs its call to your API. So I started to play with that.

Declaring a new call adapter is relatively simple:

class CustomCallAdapter<T, U>: CallAdapter<T, U> {
    // This method transforms the call into a new one
    override fun adapt(call: Call<T>): U {
        TODO("not implemented")
    }

    // This method defines the Java type used when transforming
    // the HTTP response into a Java object
    override fun responseType(): Type {
        TODO("not implemented")
    }
}

Looks relatively simple. But, as you know, a good story is made up of many twists and turns. And I was going to meet a new one.

I started to write something like:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
private annotation class AuthorisationRequired

class CustomAdatperFactory(private val account: Account): CallAdapter.Factory() {
    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? =
        object: CallAdapter<Any, Call<Any>> {
            override fun adapt(call: Call<Any>): Call<Any> {
                val annotationsClasses = annotations.map {it.annotationClass}
                if(AuthorisationRequired::class in annotationsClasses) {
                    val newRequest = call
                        .request()
                        .newBuilder()
                        .header("Authorization", "GoogleLogin auth=${account.SID}")
                        .build()

                    return ...
                }
            }
            override fun responseType(): Type = returnType
        }
}

Looks like a promising start, but what do I return? The retrofit2.Call object only have a parameterless .clone() methods and the constructor is package-private. So I do I construct a new Call object?

It turns out, call adapters are not designed to be adapted. They are designed to let you define a whole new way to perform calls by implementing the Call interface but not just dynamically modify the request.

So I did what I mostly do in this case: open a pull request. And I got to discuss with some of the nicest FOSS developers I’ve ever met in my career. Side note here: I deplore that some FOSS developers are really unsympathetic. They would close you PR or your ticket by just saying that your doing it wrong, that this is an anti-pattern or that you can just achieve what you want to achieve by writing a whole bunch of tricky code.

This is not what happened here. They just asked me what was my use-case. So I explained and they kindly gave me a very simple — though not really documented and hard to find — solution. Kudos to them! I say hard to find because it is somehow a bit counter-intuitive.

You see, Retrofit is based on another library developed by the same compagny: OkHttp which is the client Retrofit uses for performing HTTP requests. As it turns out, you can add interceptors to OkHttp to rewrite requests and process responses. At this point, there seems to be no way that OkHttp is aware of the Retrofit method that called it. Except it is. And you can retrieve the Method (and its annotations) via the Retrofit’s Invocation object. Here is how you do:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
private annotation class AuthorisationRequired

private class CustomInterceptor(private val account: Account): Interceptor {

    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        val method = chain.request().tag(Invocation::class.java)!!.method()
        if(method.isAnnotationPresent(AuthorisationRequired::class.java)) {
            // Do your stuff
        }
        return chain.proceed(chain.request())
    }
}

public interface GitHubService {
  @GET("users/{user}/repos")
  @AuthorisationRequired
  Call<List<Repo>> listRepos(@Path("user") String user);
}

val okHttpClient = OkHttpClient.Builder().addInterceptor(CustomInterceptor(account)).build()

GitHubService service = new Retrofit.Builder()
    .client(okHttpClient)
    .baseUrl("https://api.github.com/")
    .build()
    .create()

Call<List<Repo>> repos = service.listRepos("octocat")

I hope this post will be usefull for someone else. Happy Retrofit tweaking!