Intercepting and manipulating requests in Play Framework

There are situations when you have to intercept all requests coming into your application in order to do some processing prior to handling the request or doing some cleanup after it was handled. A good example that I usually give, and one that is widely used in many applications, is fishtagging for logging. This is when you write a unique identifier in the thread context and all messages are logged under the same tag (or NDC, as it was formally known in Log4j).

This is extremely useful when doing log analysis since you can easily filter all the logs that are under the same session or done by the same user. Furthermore, you usually write all the logs under the same session (so multiple requests) under the same NDC. To do this, you must intercept the request prior to it being handled by your controller.

A parallel with Spring

The exact scenario I wrote above was one I was implementing for a personal project (more on that i later articles) that I am making in Play. I developed similar things before quite a few times, but always in Spring/Spring Boot. In Spring, if you want to intercept calls to your application, you can implement a HandlerInterceptor. This interface has a preHandle(), a postHandle() and an afterCompletion() method that get called at the appropriate times by the framework.

Now, I was searching for something similar in Play. With a bit of digging through the documentation, I discovered Play’s ActionCreator. This did not have the exact structure I was expecting, but it looked promising. Instead of having a preRequest() and a postRequest() method, as in Spring, the interface only provided one method: createAction(). It takes the request and the method as a parameter and returns an Action.

How to intercept and alter the request and the response

In it’s basic implementation, a simple Action has a delegate where the request is passed and it eventually reaches your controller. The delegate returns a CompletionStage which contains the Result. Even though not as I expected, this is exactly what I needed. So, I started to experiment and eventually managed to intercept the request, store an NDC on the ThreadContext and do the cleanup before the response is returned to the user. Furthermore, I also store a session cookie if the NDC was generated on this request, so that I can re-use it for sequential requests from the same user.

A simplified version of the RequestInterceptor, one from which I stripped error handling and additional checks, looks like this:

public class RequestInterceptor implements play.http.ActionCreator {
    private static final Logger LOGGER = LogManager.getLogger(RequestInterceptor.class);

    @Override
    public Action createAction(Http.Request request, Method actionMethod) {
        return new Action.Simple() {
            @Override
            public CompletionStage<Result> call(Http.Request req) {
                CompletionStage<Result> resultCompletionStage;
                boolean ndcWasGenerated = false;
                String ndcStr = null;



                // Get the NDC cookie from the session
                Optional<String> ndc = req.session().get("NDC");
                if (ndc.isPresent()) {
                    ndcStr = ndc.get();
                } else {
                    // Generate a new NDC since no cookie was in the session. This means that it is the first request from this user
                    ndcStr = UUID.randomUUID().toString().replace("-", "");
                    ndcWasGenerated = true;
                }

                // Push the NDC in the Thread context
                ThreadContext.push(ndcStr);



                // Go down the call chain and pass the request. Eventually it will reach our controller
                resultCompletionStage = delegate.call(req);


                // Clean up the ThreadContext
                ThreadContext.pop();
                ThreadContext.clearAll();

                if (ndcWasGenerated) {
                    // If we generated the NDC, store it in a session cookie so we can use it for the next calls
                    String finalNdcStr = ndcStr;
                    return resultCompletionStage.thenApply(result -> result.addingToSession(req, "NDC", finalNdcStr));
                }
                return resultCompletionStage;
            }
        };
    }
}

After this, it was only a matter of activating the interceptor. To do this, set the variable play.http.actionCreator in your application.conf file to point to the class you just created:

play.http.actionCreator = "utils.interceptors.RequestInterceptor"


Source link