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
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
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
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
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
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.