scalaplayframeworkhttp-headershttpresponsedeadbolt-2

Play 2.5.x how to modify the content headers of the response i.e. for no cache?


Using Scala and Play 2.5.10 I implemented the following reusable action for composition and with the aim of disabling caching in the browser by changing the response headers:

import play.api.http.HeaderNames
import play.api.mvc._

import scala.concurrent.Future
import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global

case class NoCache[A](action: Action[A]) extends Action[A] with HeaderNames {
  def apply(request: Request[A]): Future[Result] = {
    action(request).andThen {
      case Success(result) => result.withHeaders(
        (CACHE_CONTROL -> "no-cache, no-store, must-revalidate"),
        (PRAGMA -> "no-cache"),
        (EXPIRES -> "0")
      )
      case Failure(result) => result
    }
  }

  lazy val parser = action.parser
}

I then reuse it in my controller action implementations like this:

def link = NoCache {
  deadbolt.SubjectPresent()() { implicit request =>
    Future {
      Ok(views.html.account.link(userService, auth))
    }
  }
}

I breakpoint into the NoCache implementation and it gets executed correctly, however, using the Web Developer Firefox plugin to monitor the network traffic I see that the response headers do not contain the "no cache" modifications ... what'm I doing wrong?


Solution

  • Problem with your code

    The problem is with andThen. andThen discards the return value. So the transformed result with new headers is getting discarded.

    Remove andThen and make it a map.

    andThen is used for running side effecting computation just after the completion of the future on which it is invoked.

    The computation of andThen returns a Future with the same result that of original future and discards the return type of the computation inside andThen.

    Here is the implementation of andThen from standard lib.

     def andThen[U](pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): Future[T] = {
        val p = Promise[T]()
        onComplete {
          case r => try pf.applyOrElse[Try[T], Any](r, Predef.conforms[Try[T]]) finally p complete r
        }
        p.future
      }
    

    Correcting your code

    case class NoCache[A](action: Action[A]) extends Action[A] with HeaderNames {
      def apply(request: Request[A]): Future[Result] = {
        action(request).map { result =>
          result.withHeaders(
            (CACHE_CONTROL -> "no-cache, no-store, must-revalidate"),
            (PRAGMA -> "no-cache"),
            (EXPIRES -> "0")
          )
        }
      }
    
      lazy val parser = action.parser
    }
    

    Other way to do that same

    You can use Play filters Filter to change the headers and also can be used for doing some pre processing and post processing work.

    You can target only specific routes by checking the uri of the request.

    import akka.stream.Materializer
    import com.google.inject.{Inject, Singleton}
    import play.api.http.DefaultHttpFilters
    import play.api.mvc.{Filter, RequestHeader, Result}
    import play.mvc.Http.HeaderNames
    
    import scala.concurrent.Future
    
    @Singleton
    class Filters @Inject() (fooFilter: FooFilter) extends DefaultHttpFilters(fooFilter) {}
    
    @Singleton
    class FooFilter @Inject() (implicit override val mat: Materializer) extends Filter {
      override def apply(f: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = {
        f(rh).map { result =>
          if (rh.uri.startsWith("/foo"))
          result.withHeaders(HeaderNames.CACHE_CONTROL -> "no-cache")
          else result
        }
      }
    }
    

    In the above example for the /foo route cache_control will be set and for the other routes same headers will be propagated.

    Note create Filters inside the root folder of the play app if not you have add the filters to the application.conf

    Never run heavy running computation inside the Filters, make Filters as light as possible.