Extending RequestContext in akka-http for fun and profit
Sometimes it’d be nice to put some additional information into RequestContext
to have it available at all times during request processing. Ideally, one could write custom directives that access this information without any need for external parameters, just like, for example, extractHost
works. I came across one such use-case when I was validating JWT tokens. I had written a custom directive that decodes the JWT token and authorizes the user:
In a nutshell, it decrypts the JWT token passed in the HTTP header into an instance of Decoded
containing the usual stuff — current user’s claims and issuer, and calls the check
function where it is decided whether the user is authorized to access a resource or not. In case the token cannot be decoded, it rejects the request. Simple enough.
You can now use this directive in your routing code like this:
This is readable and all, but may be costly because the token may be decrypted many times during processing. Alternatively, you could break up token decoding and authorization and write a separate Directive1[Decoded]
which other directives consume. There is, of course, nothing wrong with this solution, but you have to admit that there is annoying asymmetry between akka’s built-in directives and yours, which I wanted to eliminate. The reason for this is that akka-http Route
is de facto function type type Route = RequestContext ⇒ Future[RouteResult]
. Akka’s Directive[L]
can be seen as L => Route => Route
(a route transformer + extractions) so it has access to RequestContext
and can extract any property attached to the request. You cannot put your custom data into RequestContext
so all you can do is extract and pass it down the route chain — unless, of course, you can extend RequestContext
itself.
It looks like a hard thing to do. After all, RequestContext
is baked deeply into akka-http architecture. But actually, thanks to Scala’s ability to express ad hoc polymorphism, it is rather easy. Scala’s expressiveness makes it easy to achieve things that would be barely possible in weaker languages.
So, let’s do it. First, we’re going to need a new route type. Our route will be JwtRequestContext => Future[RouteResult]
Now let’s apply ad-hoc polymorphism to model that our enhanced context is still RequestContext
, implying that Route
is JwtRoute
(if this seems strange to you at first glance, just think of it as being that JwtRoute
is a route that can use both enhanced and ‘bare’ context, while Route
is restricted to using RequestContext
only).
We’re ready to construct our JwtDirective
type. As you remember, in akka-http Directive
is very similar to (T => Route) => Route
function, so in our case we need them to be (T => JwtRoute) => JwtRoute
Super. We can now write a couple of jwt-specific directives …
… but we still cannot mix them easily with akkas. Back to the drawing board. I’ve come up with the following idea: let’s introduce TopLevelJwtDirective
, which ‘from the outside’ behaves like a regular Route
, while ‘inside’ it is a JwtDirective
where our super context can be used. Thanks to the ad hoc polymorphism work we’ve already done, it is surprisingly easy to write
As you’ve probably noticed, a top-level directive can be constructed from any akka-http directive that extracts Decoded
(or Directive1[Decoded]
). You can skip this step, but I find it convenient to use akka’s built-in directives within JwtDirective
without going through hoops.
That’s basically it. To bootstrap everything, let’s write one top-level directive:
If you compare it with the original one, you’ll notice that it’s basically the same code, which means that you can mix directives freely (as long as Decoded
is extracted).
We’re still missing things like route concatenation. But that’s easy too. We just observe that not only is Route
a restricted JwtRoute
, but also JwtRoute
is a Route
as long as there is an instance of Decoded
somewhere in the scope. This instance can be then used to reconstruct JwtRequestContext
from RequestContext
.
We have brought all the pieces of the puzzle together.
We can write new specialized directives as easily as we’d write akka directives (we can use anything from akka as long as Decoded
is implicitly defined somewhere) …
… and we can mix jwt-specific routes with our routes.
I do not feel like this is a must-have in your akka-http projects. Perhaps it’s too much magic to alleviate a minor annoyance you could have lived with. Still, it may come in handy from time to time and it is nice to assure yourself that in Scala, the sky is the limit. I’d be happy to hear your thoughts.