interceptors
- 10 minutes read - 2067 wordsINTERCEPTORS ARE SO COOL!
Sometimes you need some “generic rails” that are still highly adaptable to other uses. This is the basic problem solved by the Interceptor pattern.
I really love the way OkHttp does interceptors for the generic rails of making HTTP calls, so I wanted to walk through a case study of why an interceptor might be useful and then try to synthesize some lessons & a minimal example of the pattern.
OkHttp represents several data objects corresponding to several core concepts in HTTP:
Request: a user-created data object containing details about the request being made. This is a combination of things like the URL, the HTTP method (POST), any request body, any number of request headers.Call: a client-managed object that has committed to making a certain HTTP query but which might be handled synchronously or asynchronously or canceled. They are a “preparedRequest”.Connection: a client-managed object representing the actual connection to a certain HTTP server. Used over potentially manyCalls.Response: a client-managed data object containing details about the response.
These objects are either passed to or returned from the OkHttpClient object; here’s a (slightly embellished) simple example of a synchronous GET call from the OkHttp Recipes page:
public class ExampleGet {
  private static final OkHttpClient client = new OkHttpClient();
  public static void main(String[] args) {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();
    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful())
        throw new IOException("Unexpected code " + response);
      Headers responseHeaders = response.headers();
      for (int i = 0; i < responseHeaders.size(); i++) {
        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
      }
      System.out.println(response.body().string());
    }
  }
}
You can see we construct a Request representing the HTTP call we’d like to make, hand that to our OkHttpClient instance to get back a Call, and then #execute() that to get a Response back.
Behind the scenes, OkHttpClient is managing the Connection object (and a bunch of other details) for you.
I want you to imagine you’re writing some application that calls this code on a TON of different URLs.
You might be tempted to refactor the above implementation by extracting a doCall method like
  public static void main(String[] args) {
    ExampleGet eg = new ExampleGet();
    eg.doCall("https://publicobject.com/helloworld.txt");
    eg.doCall("< ... many more urls ...>");
  }
  public String doCall(String url) {
    Request request = new Request.Builder().url(url).build();
    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) 
        throw new IOException("Unexpected code " + response);
      Headers responseHeaders = response.headers();
      for (int i = 0; i < responseHeaders.size(); i++) {
        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
      }
      String result = response.body().string();
      System.out.println(result);
      return result;
    }
  }
That seems ok, but maybe eventually our application needs to do a call to a certain domain to establish that something doesn’t exist; this implies the
if (!response.isSuccessful()) 
  throw new IOException("Unexpected code " + response);
is no longer appropriate, because we expect a 404!
We could just remove that check, and add it back at every call site that needs it. But then you live in fear of every diff you review that someone has forgotten to check!
We could add a boolean requireSuccess parameter to our #doCall method, but then all the call sites need to change.
A good IDE can probably make this relatively easy, but that doesn’t erase the fact that now our entire codebase has to be aware of our requireSuccess shenanigans.
We could get around that by maintaining the existing doCall(String) method by just delegating to our 2-arg method:
String doCall(String url) {
  return doCall(url, true);
}
String doCall(String url, boolean requireSuccess) {
  // ...
}
but let’s take a second to see if we can figure out a better way.
We originally said we only needed this behavior for a specific domain, so we don’t actually need to pass the requireSuccess at all – we can compute it inside our #doCall method:
Set<String> ERROR_DOMAINS = ImmutableSet.of(
  "traviscj.com",
  // ...
  );
String doCall(String url) {
  boolean requireSuccess = ERROR_DOMAINS.stream()
    .anyMatch(url::contains);
  // ...
}
So this is pretty cool: as long as our codebase uniformly uses our #doCall method, we’re pretty solid, right?
Time marches onward: product requirements evolve, team members churn, application ownership changes hands a few times.
Eventually, somebody needs to do something else slightly different than our ExampleGet#doCall method supports: maybe they need the async call pattern, maybe they need to add some special headers for authentication, maybe they want to remove a certain sensitive header from printing out into unsecured logs… who knows.
Maybe they just forgot about or were never taught about the ExampleGet object.
Maybe they got tired of maintaining our ExampleGet wrapper object, or got burned by its lack of tests… or got intimidated by the volume of already-existing tests, which have a bunch of twists and turns to get our HTTP request handling just right.
The end result tends to be the same:
Someone ends up throwing away the framework and just using OkHttpClient directly again!
This might feel like a breakdown in the natural order of the universe, but really this might represent an improvement in the situation for our application maintainers:
OkHttpprobably has better docs than your crappy helper/wrapper object.OkHttpprobably has better tests than your crappy helper/wrapper object.- You can hire someone that already knows the 
OkHttpAPI and they can hit the ground running. 
So we’re left with a bit of a paradox: How do we add behavior to the HTTP traffic flowing through our app without needing either:
- to explicitly apply that behavior at every call site, or…
 - our own mastermind object that knows about every behavior any of our application’s traffic requires?
 
The answer is interceptors! It lets us write a simple bit of logic like
class RequireSuccessInterceptor implements okhttp3.Interceptor {
  Set<String> ERROR_DOMAINS = ImmutableSet.of(
    "traviscj.com",
    // ...
  );
  
  public Response intercept(Chain chain) {
    Request request = chain.request();
    Response response = chain.proceed(request);
    if (!isErrorDomain(request) && !response.isSuccessful()) {
      throw new IOException("Unexpected code " + response);
    }
    return response;
  }
  
  boolean isErrorDomain(Request request) {
    String domain = request.httpUrl().topPrivateDomain();
    return ERROR_DOMAINS.contains(domain);
  }
}
I haven’t defined Chain yet, but we’ll discuss it in more detail as soon as we’ve savored what we accomplished here.
With RequireSuccessInterceptor in hand, we can then configure our OkHttpClient object to apply this behavior on all requests processed with something like
OkHttpClient client = new OkHttpClient.Builder()
  .addInterceptor(new RequireSuccessInterceptor())
  .build();
With just that, now anything using this instance of client will automatically apply this behavior!
- We didn’t need to add the error handling at every call site!
 - we didn’t need to introduce the wrapper object, so we don’t need to test that beast or train new teammates on it, they can simply read the 
OkHttpdocumentation! - We can write tests against just this logic – we don’t need to consider other strange interactions going on with other crazy functions that other teams needed to add to our wrapper object.
 - We didn’t need to try to convince jwilson that this would be a good feature for 
OkHttpin general. - In fact, Jesse probably isn’t even aware of your interceptor implementation!
 
All of these are pretty great wins!
Interceptor interface
As you probably guessed from the RequireSuccessInterceptor, the Interceptor & Chain APIs come together into:
interface Interceptor {
  Response intercept(Chain chain);
  interface Chain {
    Response proceed(Request request);
    // context of current request:
    Request request();
    Call call();
    Connection connection();
    // settings for the current request:
    int connectTimeoutMillis();
    int readTimeoutMillis();
    int writeimeoutMillis();
    
    // override settings for the current request:
    Chain withConnectTimeout(Integer timeout, TimeUnit unit);
    Chain withReadTimeout(Integer timeout, TimeUnit unit);
    Chain withWriteTimeout(Integer timeout, TimeUnit unit);
  }
}
Note that Chain consolidates all of the required details into a single object, which keeps details like exactly what those details are out of Interceptor implementations that don’t need them.
If the benefit isn’t quite clear yet, imagine Interceptor#intercept taking instances of Request, Call, and Connection as explicit method arguments – it maybe wouldn’t be too terrible in this instance, since each of these nouns is well understood and likely to be stable over time, but is that always true?
Do you really want to force all of your details on every integrator/customer?
One aspect I find really fascinating about this is that the Request/Connection breakdown neatly scopes request-scoped data separately from “connection”/session-scoped data.
That is, it exposes data from multiple scopes without conflating/muddling them together.
I think it also does a good job of exposing an enormous breadth of information: we know details not just about the Request we’re trying to send, but also about whether we’re using a proxy and details about the proxy when relevant, how the TLS handshake went down, what protocol we’re connecting over, and even the socket we’re sending data over.
This level of depth actually suffices for many critical/core OkHttp functionalities to be implemented as Interceptors, like CallServerInterceptor, ConnectInterceptor, and CacheInterceptor!
This combination of simplicity & completeness is probably worth emulating in your own Chain objects!
Chain also has another super-important job: it needs to manage the order of application of each Interceptor bound into our OkHttpClient instance.
This might not quite become clear until you investigate the implementation in RealInterceptorChain.
RealInterceptorChain
Distilling the RealInterceptorChain logic down, basically we need to do several things:
- loop over each 
Interceptorin theList<Interceptor>passed to the constructor. - maintain copies of any objects that the 
Interceptors need – here, this includesCall/Connection/Requestand the assorted timeouts. - maintain just enough state to enforce some invariants, like “all interceptors are called” or “network interceptors must be called exactly once”.
 - emit helpful error messages if any violations of those invariants are detected.
 - enforce any other important invariants, like “interceptors must return response bodies”.
 
The implementation of this is pretty clever:
override fun proceed(request: Request): Response {
  check(index < interceptors.size)
  calls++
  if (exchange != null) {
    check(exchange.finder.sameHostAndPort(request.url)) {
      "network interceptor ${interceptors[index - 1]} must retain the same host and port"
    }
    check(calls == 1) {
      "network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
    }
  }
  // Call the next interceptor in the chain.
  val next = copy(index = index + 1, request = request)
  val interceptor = interceptors[index]
  val response = interceptor.intercept(next) ?: throw NullPointerException(
      "interceptor $interceptor returned null")
  if (exchange != null) {
    check(index + 1 >= interceptors.size || next.calls == 1) {
      "network interceptor $interceptor must call proceed() exactly once"
    }
  }
  check(response.body != null) { "interceptor $interceptor returned a response with no body" }
  return response
}
minimal example
Finally, let’s see if we can make up a minimal example!
I’m going to drop Call/Connection but introduce a couple placeholders for Req and Resp, with both just holding a simple message:
class Req:
   def __init__(self, msg):
      self.msg = msg
   def __repr__(self):
      return f"Req(msg={self.msg})"
class Resp:
   def __init__(self, msg):
      self.msg = msg
   def __repr__(self):
      return f"Resp(msg={self.msg})"
In this simpler world, our Chain object only needs two methods:
class Chain:
  def proceed(self, req: Req) -> Resp: pass
  def req(self) -> Req: pass
and finally, our Interceptor object just needs to take a Chain and return a Resp:
class Interceptor:
  def intercept(self, chain: Chain) -> Resp: pass
We can implement a simple Interceptor just to have one ready for testing:
class PrintReqInterceptor(Interceptor):
  def intercept(self, chain: Chain) -> Resp:
    print("Intercepted outgoing request: [", chain.req(), "]")
    return chain.proceed(chain.req())
and finally we can implement a RealInterceptorChain:
class RealInterceptorChain(Chain):
  def __init__(self, req: Req, interceptors: typing.List[Interceptor], index: int = 0, strict: bool = False):
    self._req = req
    self.interceptors = interceptors
    self.index = index
    self.strict = strict
    self.calls = 0
  def req(self) -> Req:
    return self._req
  def proceed(self, req: Req) -> Resp:
    if self.index >= len(self.interceptors): raise Exception("broken recursion")
    
    self.calls += 1
    if self.strict:
      if self.calls != 1: raise Exception("multiple calls to #proceed not allowed")
    next_chain = RealInterceptorChain(req, self.interceptors, self.index + 1, self.strict)
    interceptor = self.interceptors[self.index]
    response = interceptor.intercept(next_chain)
    if not response: raise Exception("interceptor returned None")
    if self.strict:
      if not (self.index + 1 >= len(self.interceptors) or next_chain.calls == 1):
        raise Exception("bad recursion")
    return response
and some Interceptor that knows how to actually transform Req into Resp; I’ll use a static value to illustrate, though of course a real implementation would almost certainly need to use chain to compute the actual return value:
class BaseInterceptor(Interceptor):
  def intercept(self, chain: Chain) -> Resp:
    return Resp("pong")
With all of that in hand, we’re ready to roll! This setup
req = Req("ping")
interceptors = [
  PrintReqInterceptor(),
  BaseInterceptor(),
]
rc = RealInterceptorChain(req, interceptors)
resp = rc.proceed(req)
print(resp)
will yield this output:
Intercepted outgoing request: [ Req(msg=ping) ]
Resp(msg=pong)
just as we wanted!