Documentation

§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:

In contrast, action composition is intended for route specific concerns, such as authentication and authorisation, 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 minimise 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 java.util.concurrent.CompletionStage;
import java.util.function.Function;
import javax.inject.Inject;
import akka.stream.Materializer;
import play.Logger;
import play.mvc.*;

public class LoggingFilter extends Filter {

    @Inject
    public LoggingFilter(Materializer mat) {
        super(mat);
    }

    @Override
    public CompletionStage<Result> apply(
            Function<Http.RequestHeader, CompletionStage<Result>> nextFilter,
            Http.RequestHeader requestHeader) {
        long startTime = System.currentTimeMillis();
        return nextFilter.apply(requestHeader).thenApply(result -> {
            long endTime = System.currentTimeMillis();
            long requestTime = endTime - startTime;

            Logger.info("{} {} took {}ms and returned {}",
                requestHeader.method(), requestHeader.uri(), requestTime, result.status());

            return result.withHeader("Request-Time", "" + requestTime);
        });
    }
}

Let’s understand what’s happening here. The first thing to notice is the signature of the apply method. The first parameter, nextFilter, is a function that takes a request header and produces a result. The second parameter, requestHeader, is 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 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 CompletionStage<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 thenApply 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.withHeader("Request-Time", "" + requestTime).

§Using filters

The simplest way to use a filter is to provide an implementation of the HttpFilters interface in the root package called Filters. Typically you should extend the DefaultHttpFilters class and pass your filters to the varargs constructor:

import play.mvc.EssentialFilter;
import play.http.DefaultHttpFilters;
import play.filters.gzip.GzipFilter;
import javax.inject.Inject;

public class Filters extends DefaultHttpFilters {
  @Inject
  public Filters(GzipFilter gzip, LoggingFilter logging) {
    super(gzip, logging);
  }
}

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

§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 a `HttpRequestHandler` instead.

Since filters are applied after routing is done, it is possible to access routing information from the request, via the tags 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 java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.Map;
import javax.inject.Inject;
import akka.stream.Materializer;
import play.Logger;
import play.mvc.*;
import play.routing.Router.Tags;

public class RoutedLoggingFilter extends Filter {

    @Inject
    public RoutedLoggingFilter(Materializer mat) {
        super(mat);
    }

    @Override
    public CompletionStage<Result> apply(
            Function<Http.RequestHeader, CompletionStage<Result>> nextFilter,
            Http.RequestHeader requestHeader) {
        long startTime = System.currentTimeMillis();
        return nextFilter.apply(requestHeader).thenApply(result -> {
            Map<String, String> tags = requestHeader.tags();
            String actionMethod = tags.get(Tags.ROUTE_CONTROLLER) +
                "." + tags.get(Tags.ROUTE_ACTION_METHOD);
            long endTime = System.currentTimeMillis();
            long requestTime = endTime - startTime;

            Logger.info("{} took {}ms and returned {}",
                actionMethod, requestTime, result.status());

            return result.withHeader("Request-Time", "" + requestTime);
        });
    }
}

Note: Routing tags are a feature of the Play router. If you use a custom router these parameters may not be available.

§More powerful filters

Play provides a lower level filter API called EssentialFilter that 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 java.util.concurrent.Executor;
import javax.inject.Inject;
import akka.util.ByteString;
import play.Logger;
import play.libs.streams.Accumulator;
import play.mvc.*;

public class EssentialLoggingFilter extends EssentialFilter {

    private final Executor executor;

    @Inject
    public EssentialLoggingFilter(Executor executor) {
        super();
        this.executor = executor;
    }

    @Override
    public EssentialAction apply(EssentialAction next) {
        return EssentialAction.of(request -> {
            long startTime = System.currentTimeMillis();
            Accumulator<ByteString, Result> accumulator = next.apply(request);
            return accumulator.map(result -> {
                long endTime = System.currentTimeMillis();
                long requestTime = endTime - startTime;

                Logger.info("{} {} took {}ms and returned {}",
                    request.method(), request.uri(), requestTime, result.status());

                return result.withHeader("Request-Time", "" + requestTime);
            }, executor);
        });
    }
}

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 this with an Akka Streams Flow using the through method some transformations to the stream if you wished. We then map the result of the iteratee and thus handle it.

Note: Although it may seem that there are two different filter APIs, there is only one, EssentialFilter. The simpler Filter API in the earlier examples extends EssentialFilter, and implements it by creating a new EssentialAction. The passed in callback makes it appear to skip the body parsing by creating a promise for the Result, while the body parsing and the rest of the action are executed asynchronously.

Next: Error handling


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request.