§Action composition
This chapter introduces several ways of defining generic action functionality.
§Custom action builders
We saw previously that there are multiple ways to declare an action - with a request parameter, without a request parameter, with a body parser etc. In fact there are more than this, as we’ll see in the chapter on asynchronous programming.
These methods for building actions are actually all defined by a trait called ActionBuilder
and the Action
object that we use to declare our actions is just an instance of this trait. By implementing your own ActionBuilder
, you can declare reusable action stacks, that can then be used to build actions.
Let’s start with the simple example of a logging decorator, we want to log each call to this action.
The first way is to implement this functionality in the invokeBlock
method, which is called for every action built by the ActionBuilder
:
import play.api.mvc._
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser)
with Logging {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
logger.info("Calling action")
block(request)
}
}
Now we can use Dependency Injection in your controller to get an instance of the LoggingAction
and use it the same way we use Action
:
class MyController @Inject() (loggingAction: LoggingAction, cc: ControllerComponents)
extends AbstractController(cc) {
def index = loggingAction {
Ok("Hello World")
}
}
Since ActionBuilder
provides all the different methods of building actions, this also works with, for example, declaring a custom body parser:
def submit: Action[String] = loggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + " bytes long")
}
§Composing actions
In most applications, we will want to have multiple action builders, some that do different types of authentication, some that provide different types of generic functionality, etc. In which case, we won’t want to rewrite our logging action code for each type of action builder, we will want to define it in a reusable way.
Reusable action code can be implemented by wrapping actions:
import play.api.mvc._
case class Logging[A](action: Action[A]) extends Action[A] with play.api.Logging {
def apply(request: Request[A]): Future[Result] = {
logger.info("Calling action")
action(request)
}
override def parser = action.parser
override def executionContext = action.executionContext
}
We can also use the Action
action builder to build actions without defining our own action class:
import play.api.mvc._
def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
logger.info("Calling action")
action(request)
}
Actions can be mixed in to action builders using the composeAction
method:
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
block(request)
}
override def composeAction[A](action: Action[A]): Logging[A] = new Logging(action)
}
Now the builder can be used in the same way as before:
def index = loggingAction {
Ok("Hello World")
}
We can also mix in wrapping actions without the action builder:
def index = Logging {
Action {
Ok("Hello World")
}
}
§More complicated actions
So far we’ve only shown actions that don’t impact the request at all. Of course, we can also read and modify the incoming request object:
import play.api.mvc._
import play.api.mvc.request.RemoteConnection
def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
val newRequest = request.headers.get("X-Forwarded-For") match {
case None => request
case Some(xff) =>
val xffConnection = RemoteConnection(xff, request.connection.secure, None)
request.withConnection(xffConnection)
}
action(newRequest)
}
Note: Play already has built in support for
X-Forwarded-For
headers.
We could block the request:
import play.api.mvc._
import play.api.mvc.Results._
def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
request.headers
.get("X-Forwarded-Proto")
.collect {
case "https" => action(request)
}
.getOrElse {
Future.successful(Forbidden("Only HTTPS requests allowed"))
}
}
And finally we can also modify the returned result:
import play.api.mvc._
def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}
§Different request types
While action composition allows you to perform additional processing at the HTTP request and response level, often you want to build pipelines of data transformations that add context to or perform validation on the request itself. ActionFunction
can be thought of as a function on the request, parameterized over both the input request type and the output type passed on to the next layer. Each action function may represent modular processing such as authentication, database lookups for objects, permission checks, or other operations that you wish to compose and reuse across actions.
There are a few pre-defined traits implementing ActionFunction
that are useful for different types of processing:
ActionTransformer
can change the request, for example by adding additional information.ActionFilter
can selectively intercept requests, for example to produce errors, without changing the request value.ActionRefiner
is the general case of both of the above.ActionBuilder
is the special case of functions that takeRequest
as input, and thus can build actions.
You can also define your own arbitrary ActionFunction
by implementing the invokeBlock
method. Often it is convenient to make the input and output types instances of Request
(using WrappedRequest
), but this is not strictly necessary.
§Authentication
One of the most common use cases for action functions is authentication. We can easily implement our own authentication action transformer that determines the user from the original request and adds it to a new UserRequest
. Note that this is also an ActionBuilder
because it takes a simple Request
as input:
import play.api.mvc._
class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)
class UserAction @Inject() (val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent]
with ActionTransformer[Request, UserRequest] {
def transform[A](request: Request[A]) = Future.successful {
new UserRequest(request.session.get("username"), request)
}
}
Play also provides a built in authentication action builder. Information on this and how to use it can be found here.
Note: The built in authentication action builder is just a convenience helper to minimize the code necessary to implement authentication for simple cases, its implementation is very similar to the example above.
Since it is simple to write your own authentication helper, we recommend doing so if the built-in helper does not suit your needs.
§Adding information to requests
Now let’s consider a REST API that works with objects of type Item
. There may be many routes under the /item/:itemId
path, and each of these need to look up the item. In this case, it may be useful to put this logic into an action function.
First of all, we’ll create a request object that adds an Item
to our UserRequest
:
import play.api.mvc._
class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
def username = request.username
}
Now we’ll create an action refiner that looks up that item and returns Either
an error (Left
) or a new ItemRequest
(Right
). Note that this action refiner is defined inside a method that takes the id of the item:
def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest] {
def executionContext = ec
def refine[A](input: UserRequest[A]): Future[Either[Status, ItemRequest[A]]] = Future.successful {
ItemDao
.findById(itemId)
.map(new ItemRequest(_, input))
.toRight(NotFound)
}
}
§Validating requests
Finally, we may want an action function that validates whether a request should continue. For example, perhaps we want to check whether the user from UserAction
has permission to access the item from ItemAction
, and if not return an error:
def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequest] {
def executionContext = ec
def filter[A](input: ItemRequest[A]) = Future.successful {
if (!input.item.accessibleByUser(input.username))
Some(Forbidden)
else
None
}
}
§Putting it all together
Now we can chain these action functions together (starting with an ActionBuilder
) using andThen
to create an action:
def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext): Action[AnyContent] =
userAction.andThen(ItemAction(itemId)).andThen(PermissionCheckAction) { request =>
request.item.addTag(tag)
Ok("User " + request.username + " tagged " + request.item.id)
}
Play also provides a global filter API , which is useful for global cross cutting concerns.
§Action composition in interaction with body parsing
By default body parsing takes place before action composition happens, meaning you are able to access the already parsed request body inside every action via request.body()
. However, there are use cases where it makes sense to defer body parsing after some (or all) actions defined via action composition have been processed. For example:
- When you want to pass request specific information to the body parser via request attributes. E.g. user dependent maximum file upload size or user dependent credentials for a webservice or object storage where the body parser should redirect an upload to.
- When using action composition for (granular) authorization you may dont want to even parse the request body and cancel the request early if permission checks fail.
Of course, when deferring body parsing, the request body won’t be parsed yet inside actions that are executed before body parsing takes place and therefore request.body()
will return null
.
You can enable deferred body parsing globally in conf/application.conf
:
play.server.deferBodyParsing = true
Just be aware that, like all play.server.*
config keys, this config won’t be picked up by Play when running in DEV mode, but only in PROD mode. To set this config in DEV mode you have to set it in build.sbt
:
PlayKeys.devSettings += "play.server.deferBodyParsing" -> "true"
Instead of enabling deferred body parsing globally, you can enable it just for specific routes by using the routes modifier deferBodyParsing
:
+ deferBodyParsing
POST / controllers.HomeController.uploadFileToS3
The opposite is true as well. If you globally enable deferred body parsing you can disable it for specific routes by using the routes modifier dontDeferBodyParsing
:
+ dontDeferBodyParsing
POST / controllers.HomeController.processUpload
The body can now be parsed by calling play.api.mvc.BodyParser.parseBody
:
def home(): Action[AnyContent] = Action.async(parse.default) { implicit request: Request[AnyContent] =>
{
// When body parsing was deferred, the body is not parsed here yet, so following will be true:
// - request.body == null
// - request.attrs.contains(play.api.mvc.request.RequestAttrKey.DeferredBodyParsing)
// Do NOT rely on request.hasBody because it has nothing to do if a body was parsed or not!
BodyParser.parseBody(
parse.default,
request,
(req: Request[AnyContent]) => {
// The body is parsed here now, therefore:
// - request.body has a value now
// - request.attrs does not contain RequestAttrKey.DeferredBodyParsing anymore
Future.successful(Ok)
}
)
}
}
Next: Content negotiation