§Filters
Play provides a simple filter API for applying global filters to each request.
§Filters vs action composition
The filter API is intended for cross cutting concerns that are applied indiscriminately to all routes. For example, here are some common use cases for filters:
- Logging/metrics collection
- GZIP encoding
- Security headers
In contrast, action composition is intended for route specific concerns, such as authentication and authorization, caching and so on. If your filter is not one that you want applied to every route, consider using action composition instead, it is far more powerful. And don’t forget that you can create your own action builders that compose your own custom defined sets of actions to each route, to minimize boilerplate.
§A simple logging filter
The following is a simple filter that times and logs how long a request takes to execute in Play Framework, which implements the Filter
trait:
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.pekko.stream.Materializer
import play.api.mvc._
import play.api.Logging
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter with Logging {
def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
nextFilter(requestHeader).map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(
s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}"
)
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
Let’s understand what’s happening here. The first thing to notice is the signature of the apply
method. It’s a curried function, with the first parameter, nextFilter
, being a function that takes a request header and produces a result, and the second parameter, requestHeader
, being the actual request header of the incoming request.
The nextFilter
parameter represents the next action in the filter chain. Invoking it will cause the action to be invoked. In most cases you will probably want to invoke this at some point in your future. You may decide to not invoke it if for some reason you want to block the request.
We save a timestamp before invoking the next filter in the chain. Invoking the next filter returns a Future[Result]
that will redeemed eventually. Take a look at the Handling asynchronous results chapter for more details on asynchronous results. We then manipulate the Result
in the Future
by calling the map
method with a closure that takes a Result
. We calculate the time it took for the request, log it and send it back to the client in the response headers by calling result.withHeaders("Request-Time" -> requestTime.toString)
.
§Using filters
The simplest way to use a filter is to provide an implementation of the HttpFilters
trait in the root package. If you’re using Play’s runtime dependency injection support (such as Guice) you can extend the DefaultHttpFilters
class and pass your filters to the varargs constructor:
import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import play.api.http.EnabledFilters
import play.filters.gzip.GzipFilter
class Filters @Inject() (
defaultFilters: EnabledFilters,
gzip: GzipFilter,
log: LoggingFilter
) extends DefaultHttpFilters(defaultFilters.filters :+ gzip :+ log: _*)
If you want to have different filters in different environments, or would prefer not putting this class in the root package, you can configure where Play should find the class by setting play.http.filters
in application.conf
to the fully qualified class name of the class. For example:
play.http.filters=com.example.MyFilters
If you’re using BuiltInComponents
for compile-time dependency injection, you can simply override the httpFilters
lazy val:
import play.api._
import play.filters.gzip._
import play.filters.HttpFiltersComponents
import router.Routes
class MyComponents(context: ApplicationLoader.Context)
extends BuiltInComponentsFromContext(context)
with HttpFiltersComponents
with GzipFilterComponents {
// implicit executionContext and materializer are defined in BuiltInComponents
lazy val loggingFilter: LoggingFilter = new LoggingFilter()
// gzipFilter is defined in GzipFilterComponents
override lazy val httpFilters = Seq(gzipFilter, loggingFilter)
lazy val router: Routes = new Routes( /* ... */ )
}
The filters provided by Play all provide traits that work with BuiltInComponents
:
- GzipFilterComponents
- CSRFComponents
- CORSComponents
- SecurityHeadersComponents
- AllowedHostsComponents
§Where do filters fit in?
Filters wrap the action after the action has been looked up by the router. This means you cannot use a filter to transform a path, method or query parameter to impact the router. However you can direct the request to a different action by invoking that action directly from the filter, though be aware that this will bypass the rest of the filter chain. If you do need to modify the request before the router is invoked, a better way to do this would be to place your logic in `HttpRequestHandler` instead.
Since filters are applied after routing is done, it is possible to access routing information from the request, via the attrs
map on the RequestHeader
. For example, you might want to log the time against the action method. In that case, you might update the filter to look like this:
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.pekko.stream.Materializer
import play.api.mvc.Filter
import play.api.mvc.RequestHeader
import play.api.mvc.Result
import play.api.routing.HandlerDef
import play.api.routing.Router
import play.api.Logging
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter with Logging {
def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
nextFilter(requestHeader).map { result =>
val handlerDef: HandlerDef = requestHeader.attrs(Router.Attrs.HandlerDef)
val action = handlerDef.controller + "." + handlerDef.method
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(s"${action} took ${requestTime}ms and returned ${result.header.status}")
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
Routing attributes are a feature of the Play router. If you use a custom router, or return a custom action through a custom request handler, these parameters may not be available.
§More powerful filters
Play provides a lower level filter API called EssentialFilter
which gives you full access to the body of the request. This API allows you to wrap EssentialAction with another action.
Here is the above filter example rewritten as an EssentialFilter
:
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import org.apache.pekko.util.ByteString
import play.api.libs.streams.Accumulator
import play.api.mvc._
import play.api.Logging
class LoggingFilter @Inject() (implicit ec: ExecutionContext) extends EssentialFilter with Logging {
def apply(nextFilter: EssentialAction): EssentialAction = new EssentialAction {
def apply(requestHeader: RequestHeader) = {
val startTime = System.currentTimeMillis
val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader)
accumulator.map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(
s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}"
)
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
}
The key difference here, apart from creating a new EssentialAction
to wrap the passed in next
action, is when we invoke next, we get back an Accumulator
.
You could compose the Accumulator
with an Pekko Streams Flow using the through
method with some transformations to the stream if you wished. We then map
the result of the iteratee and thus handle it.
class AccumulatorFlowFilter @Inject() (actorSystem: ActorSystem)(implicit ec: ExecutionContext)
extends EssentialFilter {
private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")
private implicit val logging: LoggingAdapter = Logging(actorSystem.eventStream, logger.getName)
override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
val accumulator: Accumulator[ByteString, Result] = next(request)
val flow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString].log("byteflow")
val accumulatorWithResult = accumulator.through(flow).map { result =>
logger.info(s"The flow has completed and the result is $result")
result
}
accumulatorWithResult
}
}
}
Although it may seem that there are two different filter APIs, there is only one,
EssentialFilter
. The simplerFilter
API in the earlier examples extendsEssentialFilter
, and implements it by creating a newEssentialAction
. The passed in callback makes it appear to skip the body parsing by creating a promise for theResult
, while the body parsing and the rest of the action are executed asynchronously.
Next: Testing your application