The request-response life cycle

The Play Framework uses Netty by default, so requests are received by NettyServer.

Netty allows a variety of actions including custom coding through handlers. We can define a handler that transforms a request into a desired response and provides it to Netty when bootstrapping the application. To integrate a Play app with Netty, PlayDefaultUpstreamHandler is used.

Note

For additional information on requests used in Netty, refer to Netty docs at http://netty.io/wiki/user-guide-for-4.x.html and Netty ChannelPipeline docs at http://netty.io/4.0/api/io/netty/channel/ChannelPipeline.html.

PlayDefaultUpstreamHandler extends org.jboss.netty.channel.SimpleChannelUpstreamHandler to handle both HTTP and WebSocket requests. It is used when bootstrapping the application to Netty in the following way:

val defaultUpStreamHandler = new PlayDefaultUpstreamHandler(this, allChannels)

The messageReceived method of SimpleChannelUpStreamHandler is responsible for acting on the received request. PlayDefaultUpstreamHandler overwrites this so that requests are sent to our application. This method is too long (around 260 lines, including comments and blank lines), so we will only look at relevant blocks here.

First, a Play RequestHeader is created for the message received and its corresponding action is found:

val (requestHeader, handler: Either[Future[Result], (Handler, Application)]) = Exception.allCatch[RequestHeader].either {
    val rh = tryToCreateRequest
            // Force parsing of uri
            rh.path
            rh
          }.fold(
            e => {
              //Exception Handling
              ...
            },
            rh => server.getHandlerFor(rh) match {
              case directResult @ Left(_) => (rh, directResult)
              case Right((taggedRequestHeader, handler, application)) => (taggedRequestHeader, Right((handler, application)))
            }
          )

In the preceding snippet, the tryToCreateRequest method results in RequestHeader and any exceptions encountered in this process are handled. The action for the RequestHeader rh is then fetched through server.getHandlerFor(rh). Here, a server is an instance of the server trait and the getHandlerFor method utilizes the application's global object and its onRequestReceived method:

try {
        applicationProvider.get.map { application =>
          application.global.onRequestReceived(request) match {
            case (requestHeader, handler) => (requestHeader, handler, application)
          }
        }
      } catch {
  //Exception Handling
...
}

In the messageReceived method of PlayDefaultUpstreamHandler, the action obtained from server.getHandlerFor is eventually called, resulting in a response.

Most of the interactions of PlayDefaultUpStreamHandler with the application are through its global object. In the following section, we will see the methods available in GlobalSettings related to the request-response life cycle.

Fiddling with the request-response life cycle

The GlobalSettings trait has methods related to different stages of the application's life cycle as well as its request-response life cycle. Using the request-related hooks, we can define business logic when a request is received, when an action is not found for the request, and so on.

The request-related methods are as follows:

  • onRouteRequest: This uses a router to identify the action for a given RequestHeader
  • onRequestReceived: This results in RequestHeader and its action. Internally, it calls the onRouteRequest method
  • doFilter: This adds a filter to the application
  • onError: This is a method that handles exceptions when processing
  • onHandlerNotFound: This is used when a RequestHeader's corresponding action cannot be found
  • onBadRequest: This is used internally when the request body is incorrect
  • onRequestCompletion: This is used to perform operations after a request has been processed successfully

Manipulating requests and their responses

In some applications, it is mandatory to filter, modify, redirect requests, and their responses. Consider these examples:

  • Requests for any service must have headers that contain session details and user identities except for instances, such as logins, registers, and forgetting passwords
  • All requests made for a path starting with admin must be restricted by the user role
  • Redirect requests to regional sites if possible (such as Google)
  • Add additional fields to the request or response

The onRequestReceived, onRouteRequest, doFilter, and onRequestCompletion methods can be used to intercept the request or its response and manipulate them as per requirements.

Let's look at the onRequestReceived method:

def onRequestReceived(request: RequestHeader): (RequestHeader, Handler) = {
    val notFoundHandler = Action.async(BodyParsers.parse.empty)(this.onHandlerNotFound)
    val (routedRequest, handler) = onRouteRequest(request) map {
      case handler: RequestTaggingHandler => (handler.tagRequest(request), handler)
      case otherHandler => (request, otherHandler)
    } getOrElse {
    // We automatically permit HEAD requests against any GETs without the need to
      // add an explicit mapping in Routes
      val missingHandler: Handler = request.method match {
        case HttpVerbs.HEAD =>
          new HeadAction(onRouteRequest(request.copy(method = HttpVerbs.GET)).getOrElse(notFoundHandler))
        case _ =>
          notFoundHandler
      }
      (request, missingHandler)
    }

    (routedRequest, doFilter(rh => handler)(routedRequest))
  }

It fetches the corresponding handler for a given RequestHeader using the onRouteRequest and doFilter methods. If no handler is found, the result from onHandlerNotFound is sent.

Since the onRequestReceived method plays a critical role in how the requests are processed, sometimes it may be simpler to override the onRouteRequest method.

The onRouteRequest method is defined as follows:

def onRouteRequest(request: RequestHeader): Option[Handler] = Play.maybeApplication.flatMap(_.routes.flatMap {
    router =>
      router.handlerFor(request)
  })

Here, the router is the application's router object. By default, it is the generated object created from conf/routes on compilation. A router extends the Router.Routes trait and the handlerFor method is defined in this trait.

Let's try to implement a solution for blocking requests to services other than login, forgotPassword, and register if the request header does not have the session and user details. We can do so by overriding onRouteRequest:

override def onRouteRequest(requestHeader: RequestHeader) = {
    val path = requestHeader.path

    val pathConditions = path.equals("/") ||
      path.startsWith("/register") ||
      path.startsWith("/login") ||
      path.startsWith("/forgot")

   if (!pathConditions) {
      val tokenId = requestHeader.headers.get("Auth-Token")
      val userId = requestHeader.headers.get("Auth-User")
      if (tokenId.isDefined && userId.isDefined) {
        val isValidSession = SessionDetails.validateSession(SessionDetails(userId.get.toLong, tokenId.get))
        if (isValidSession) {
          super.onRouteRequest(request)
        }
        else Some(controllers.SessionController.invalidSession)
      }
      else {
        Some(controllers.SessionController.invalidSession)
      }
    }
    else {
      super.onRouteRequest(request)
    }
  }

First, we check if the requested path has restricted access. If so, we check if the necessary headers are available and valid. Only then is the corresponding Handler returned, else Handler for an invalid session is returned. A similar approach can be followed if we need to control the access based on the user's role.

We can also use the onRouteRequest method to provide compatibility for older deprecated services. For example, if the older version of the application had a GET /user/:userId service that has now been modified to /api/user/:userId, and there are other applications that rely on this application, our application should support requests for both the paths. However, the routes file only lists the new paths and services, which means that we should handle these before attempting to access the application's supported routes:

override def onRouteRequest(requestHeader: RequestHeader) = {
  val path = requestHeader.path

  val actualPath = getSupportedPath(path)
  val customRequestHeader = requestHeader.copy(path = actualPath)
   
  super.onRouteRequest(customRequestHeader)
}

The getSupportedPath is a custom method that gives a new path for a given old path. We create a new RequestHeader with the updated fields and forward this to the following methods instead of the original RequestHeader.

Similarly, we could add/modify the headers or any other field(s) of RequestHeader.

The doFilter method can be used to add filters, similar to those shown in Chapter 2, Defining Actions:

object Global extends GlobalSettings {
  override def doFilter(action: EssentialAction): EssentialAction = HeadersFilter.noCache(action)
}

Alternatively, we can extend the WithFilters class instead of GlobalSettings:

object Global extends WithFilters(new CSRFFilter()) with GlobalSettings

The WithFilters class extends GlobalSettings and overrides the doFilter method with the Filter passed in its constructor. It is defined as follows:

class WithFilters(filters: EssentialFilter*) extends GlobalSettings {
  override def doFilter(a: EssentialAction): EssentialAction = {
    Filters(super.doFilter(a), filters: _*)
  }
}

The onRequestCompletion method can be used to perform specific tasks after a request has been processed. For example, suppose that the application needs a requirement to persist data from specific GET requests, such as Search. This can come in handy to understand and analyze what the users are looking for in our application. Persisting information from requests prior to fetching data can considerably increase the response time and hamper user experience. Therefore, it will be better if this is done after the response has been sent:

override def onRequestCompletion(requestHeader: RequestHeader) {
  if(requestHeader.path.startsWith("/search")){
    //code to persist request parameters, time, etc
  }}

Tackling errors and exceptions

An application cannot exist without handling errors and exceptions. Based on the business logic, the way they are handled may differ from application to application. Play provides certain standard implementations which can be overridden in the application's global object. The onError method is called when an exception occurs and is defined as follows:

  def onError(request: RequestHeader, ex: Throwable): Future[Result] = {
    def devError = views.html.defaultpages.devError(Option(System.getProperty("play.editor"))) _
    def prodError = views.html.defaultpages.error.f
    try {
      Future.successful(InternalServerError(Play.maybeApplication.map {
        case app if app.mode == Mode.Prod => prodError
        case app => devError
      }.getOrElse(devError) {
        ex match {
          case e: UsefulException => e
          case NonFatal(e) => UnexpectedException(unexpected = Some(e))
        }
      }))
    } catch {
      case NonFatal(e) => {
        Logger.error("Error while rendering default error page", e)
        Future.successful(InternalServerError)
      }
    }
  }

UsefulException is an abstract class, which extends RuntimeException. It is extended by the PlayException helper. The default implementation of onError (in the previous code snippet) simply checks whether the application is in the production mode or in the development mode and sends the corresponding view as Result. This method results in the defaultpages.error or defaultpages.devError view.

Suppose we want to send a response with a status 500 and the exception instead. We can easily do so by overriding the onError method:

override def onError(request: RequestHeader, ex: Throwable) = {
  log.error(ex)
  InternalServerError(ex.getMessage)
}

The onHandlerNotFound method is called when a user sends a request with a path that is not defined in conf/routes. It is defined as follows:

def onHandlerNotFound(request: RequestHeader): Future[Result] = {
  Future.successful(NotFound(Play.maybeApplication.map {
    case app if app.mode != Mode.Prod => views.html.defaultpages.devNotFound.f
    case app => views.html.defaultpages.notFound.f
  }.getOrElse(views.html.defaultpages.devNotFound.f)(request, Play.maybeApplication.flatMap(_.routes))))
  }

It sends a view as a response, depending on the mode in which the application was started. In the development mode, the view contains an error message, which tells us that an action is defined for the route and the list of supported paths with the request type. We can override this, if required.

The onBadRequest method is called in the following situations:

  • The request is sent and its corresponding action has a different content type
  • Some of the parameters are missing in the request sent and, when parsing, the request throws an exception

It is defined as follows:

def onBadRequest(request: RequestHeader,
  error: String): Future[Result] = {
    Future.successful(BadRequest(views.html.defaultpages.badRequest(request, error)))
}

This method also sends a view in response but, in most applications, we would like to send BadRequest with the error message and not the view. This can be achieved by overriding the default implementation, as follows:

import play.api.mvc.{Result, RequestHeader,Results}
 override def onBadRequest(request: RequestHeader,
                           error: String): Future[Result] = {
    Future{
      Results.BadRequest(error)
    }
  }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.254.151