Understanding the Play Filter API
With Play 2.1 hot off the press, there have been a lot of people asking about the new Play filter API. In actual fact, the API is incredibly simple:
trait EssentialFilter {
def apply(next: EssentialAction): EssentialAction
}
Essentially, a filter is just a function that takes an action and returns another action. The usual thing that would be done by the filter is wrap the action, invoking it as a delegate. To then add a filter to your application, you just add it to your Global doFilter
method. We provide a helper class to do that for you:
object Global extends WithFilters(MyFilter) {
...
}
Easy right? Wrap the action, register it in global. Well, it is easy, but only if you understand Plays architecture. This is very important, because once you understand Play's architecture, you will be able to do far more with Play. We have some documentation here that explains Plays architecture at a high level. In this blog post, I'm going to explain Play's architecture in the context of filters, with code snippets and use cases along the way.
A short introduction to Plays architecture
I don't need to go in depth here because I've already provided a link to our architecture documentation, but in short Play's architecture matches the flow of an HTTP request very well.
The first thing that arrives when an HTTP request is made is the request header. So an action in Play therefore must be a function that accepts a request header.
What happens next in an HTTP request? The body is received. So, the function that receives the request must return something that consumes the body. This is an iteratee, which is a reactive stream handler, that eventually produces a single result after consuming the stream. You don't necessarily need to understand the details about how iteratees work in order to understand filters, the important thing to understand is that iteratees eventually produce a result that you can map, just like a future, using their map
function. For details on writing iteratees, read my blog post.
The next thing that happens in an HTTP request is that the http response must be sent. So what is the result that of the iteratee? An HTTP response. And an HTTP response is a set of response headers, followed by a response body. The response body is an enumerator, which is a reactive stream producer.
All of this is captured in Plays EssentialAction
trait:
trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])
This reads that an essential action is a function that takes a request header and returns an iteratee that consumes the byte array body chunks and eventually produces a result.
The simpler way
Before I go on, I'd like to point out that Play provides a helper trait called Filter
that makes writing filters easier than when using EssentialFilter
. This is similar to the Action
trait, in that Action
simplifies writing EssentialAction
's by not needing to worry about iteratees and how the body is parsed, rather you just provide a function that takes a request with a parsed body, and return a result. The Filter
trait simplifies things in a similar way, however I'm going to leave talking about that until the end, because I think it is better to understand how filters work from the bottom up before you start using the helper class.
The noop filter
To demonstrate what a filter looks like, the first thing I will show is a noop filter:
class NoopFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
next(request)
}
}
}
Each time the filter is executed, we create a new EssentialAction
that wraps it. Since EssentialAction
is just a function, we can just invoke it, passing the passed in request. So the above is our basic pattern for implementing an EssentialFilter
.
Handling the request header
Let's say we want to look at the request header, and conditionally invoke the wrapped action based on what we inspect. An example of a filter that would do that might be a blanket security policy for the /admin
area of your website. This might look like this:
class AdminFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
if (request.path.startsWith("/admin") && request.session.get("user").isEmpty) {
Iteratee.ignore[Array[Byte]].map(_ => Results.Forbidden())
} else {
next(request)
}
}
}
}
You can see here that since we are intercepting the action before the body has been parsed, we still need to provide a body parser when we block the action. In this case we are returning a body parser that will simply ignore the whole body, and mapping it to have a result of forbidden.
Handling the body
In some cases, you might want to do something with the body in your filter. In some cases, you might want to parse the body. If this is the case, consider using action composition instead, because that makes it possible to hook in to the action processing after the action has parsed the body. If you want to parse the body at the filter level, then you'll have to buffer it, parse it, and then stream it again for the action to parse again.
However there are some things that can be easily be done at the filter level. One example is gzip decompression. Play framework already provides gzip decompression out of the box, but if it didn't this is what it might look like (using the gunzip enumeratee from my play extra iteratees project):
class GunzipFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
if (request.headers.get("Content-Encoding").exists(_ == "gzip")) {
Gzip.gunzip() &>> next(request)
} else {
next(request)
}
}
}
}
Here using iteratee composition we are wrapping the body parser iteratee in a gunzip enumeratee.
Handling the response headers
When you're filtering you will often want to do something to the response that is being sent. If you just want to add a header, or add something to the session, or do any write operation on the response, without actually reading it, then this is quite simple. For example, let's say you wanted to add a custom header to every response:
class SosFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
next(request).map(result =>
result.withHeaders("X-Sos-Message" -> "I'm trapped inside Play Framework please send help"))
}
}
}
Using the map
function on the iteratee that handles the body, we are given access to the result produced by the action, which we can then modify as demonstrated.
If however you want to read the result, then you'll need to unwrap it. Play results are either AsyncResult
or PlainResult
. An AsyncResult
is a Result
that contains a Future[Result]
. It has a transform
method that allows the eventual PlainResult
to be transformed. A PlainResult
has a header and a body.
So let's say you want to add a timestamp to every newly created session to record when it was created. This could be done like this:
class SessionTimestampFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
def addTimestamp(result: PlainResult): Result = {
val session = Session.decodeFromCookie(Cookies(result.header.headers.get(HeaderNames.COOKIE)).get(Session.COOKIE_NAME))
if (!session.isEmpty) {
result.withSession(session + ("timestamp" -> System.currentTimeMillis.toString))
} else {
result
}
}
next(request).map {
case plain: PlainResult => addTimestamp(plain)
case async: AsyncResult => async.transform(addTimestamp)
}
}
}
}
Handling the response body
The final thing you might want to do is transform the response body. PlainResult
has two implementations, SimpleResult
, which is for bodies with no transfer encoding, and ChunkedResult
, for bodies with chunked transfer encoding. SimpleResult
contains an enumerator, and ChunkedResult
contains a function that accepts an iteratee to write the result out to.
An example of something you might want to do is implement a gzip filter. A very naive implementation (as in, do not use this, instead use my complete implementation from my play extra iteratees project) might look like this:
class GzipFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(request: RequestHeader) = {
def gzipResult(result: PlainResult): Result = result match {
case simple @ SimpleResult(header, content) => SimpleResult(header.copy(
headers = (header.headers - "Content-Length") + ("Content-Encoding" -> "gzip")
), content &> Enumeratee.map(a => simple.writeable.transform(a)) &> Gzip.gzip())
}
next(request).map {
case plain: PlainResult => gzipResult(plain)
case async: AsyncResult => async.transform(gzipResult)
}
}
}
}
Using the simpler API
Now you've seen how you can achieve everything using the base EssentialFilter
API, and hopefully therefore you understand how filters fit into Play's architecture and how you can utilise them to achieve your requirements. Let's now have a look at the simpler API:
trait Filter extends EssentialFilter {
def apply(f: RequestHeader => Result)(rh: RequestHeader): Result
def apply(next: EssentialAction): EssentialAction = {
...
}
}
object Filter {
def apply(filter: (RequestHeader => Result, RequestHeader) => Result): Filter = new Filter {
def apply(f: RequestHeader => Result)(rh: RequestHeader): Result = filter(f,rh)
}
}
Simply put, this API allows you to write filters without having to worry about body parsers. It makes it look like actions are just functions of request headers to results. This limits the full power of what you can do with filters, but for many use cases, you simply don't need this power, so using this API provides a simple alternative.
To demonstrate, a noop filter class looks like this:
class NoopFilter extends Filter {
def apply(f: (RequestHeader) => Result)(rh: RequestHeader) = {
f(rh)
}
}
Or, using the Filter
companion object:
val noopFilter = Filter { (next, req) =>
next(req)
}
And a request timing filter might look like this:
val timingFilter = Filter { (next, req) =>
val start = System.currentTimeMillis
def logTime(result: PlainResult): Result = {
Logger.info("Request took " + (System.currentTimeMillis - start))
result
}
next(req) match {
case plain: PlainResult => logTime(plain)
case async: AsyncResult => async.transform(logTime)
}
}