From dffe5433f23492c7323909bbfc7b6143ec82c527 Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Thu, 13 Feb 2020 10:38:21 -0800 Subject: [PATCH 1/2] Add Caching Convenience Functions --- .../http4s/server/middleware/Caching.scala | 142 +++++++++++++----- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/Caching.scala b/server/src/main/scala/org/http4s/server/middleware/Caching.scala index cdacbcbb3d3..28dc4cff8f2 100644 --- a/server/src/main/scala/org/http4s/server/middleware/Caching.scala +++ b/server/src/main/scala/org/http4s/server/middleware/Caching.scala @@ -26,24 +26,33 @@ object Caching { Kleisli { a: A => for { resp <- http(a) - now <- HttpDate.current[G] - } yield { - val headers = List( - `Cache-Control`( - NonEmptyList.of[CacheDirective]( - CacheDirective.`no-store`, - CacheDirective.`private`(), - CacheDirective.`no-cache`(), - CacheDirective.`max-age`(0.seconds) - )), - Header("Pragma", "no-cache"), - HDate(now), - Expires(HttpDate.Epoch) // Expire at the epoch for no time confusion - ) - resp.putHeaders(headers: _*) - } + out <- `no-store-response`[G](resp) + } yield out + } + + /** + * Transform a Response so that it will not be cached. + */ + def `no-store-response`[G[_]]: PartiallyAppliedNoStoreCache[G] = + new PartiallyAppliedNoStoreCache[G] { + def apply[F[_]](resp: Response[F])(implicit M: Monad[G], C: Clock[G]): G[Response[F]] = + HttpDate.current[G].map(now => resp.putHeaders(HDate(now) :: noStoreStaticHeaders: _*)) } + // These never change, so don't recreate them each time. + private val noStoreStaticHeaders = List( + `Cache-Control`( + NonEmptyList.of[CacheDirective]( + CacheDirective.`no-store`, + CacheDirective.`private`(), + CacheDirective.`no-cache`(), + CacheDirective.`max-age`(0.seconds) + ) + ), + Header("Pragma", "no-cache"), + Expires(HttpDate.Epoch) // Expire at the epoch for no time confusion + ) + /** * Helpers Contains the default arguments used to help construct * middleware with [[caching]]. They serve to support the default arguments for @@ -80,6 +89,15 @@ object Caching { Helpers.defaultStatusToSetOn, http) + /** + * Publicly Cache a Response for the given lifetime. + * + * Note: If set to Duration.Inf, lifetime falls back to + * 10 years for support of Http1 caches. + **/ + def publicCacheResponse[G[_]](lifetime: Duration): PartiallyAppliedCache[G] = + cacheResponse(lifetime, Either.left(CacheDirective.public)) + /** * Sets headers for response to be privately cached for the specified duration. * @@ -97,6 +115,18 @@ object Caching { Helpers.defaultStatusToSetOn, http) + /** + * Privately Caches A Response for the given lifetime. + * + * Note: If set to Duration.Inf, lifetime falls back to + * 10 years for support of Http1 caches. + **/ + def privateCacheResponse[G[_]]( + lifetime: Duration, + fieldNames: List[CaseInsensitiveString] = Nil + ): PartiallyAppliedCache[G] = + cacheResponse(lifetime, Either.right(CacheDirective.`private`(fieldNames))) + /** * Construct a Middleware that will apply the appropriate caching headers. * @@ -111,36 +141,68 @@ object Caching { methodToSetOn: Method => Boolean, statusToSetOn: Status => Boolean, http: Http[G, F] - ): Http[G, F] = { - val actualLifetime = lifetime match { - case finite: FiniteDuration => finite - case _ => 315360000.seconds // 10 years - // Http1 caches do not respect max-age headers, so to work globally it is recommended - // to explicitly set an Expire which requires some time interval to work - } + ): Http[G, F] = Kleisli { req: Request[F] => for { resp <- http(req) out <- if (methodToSetOn(req.method) && statusToSetOn(resp.status)) { - HttpDate.current[G].flatMap { now => - HttpDate - .fromEpochSecond(now.epochSecond + actualLifetime.toSeconds) - .liftTo[G] - .map { expires => - val headers = List( - `Cache-Control`( - NonEmptyList.of( - isPublic.fold[CacheDirective](identity, identity), - CacheDirective.`max-age`(actualLifetime) - )), - HDate(now), - Expires(expires) - ) - resp.putHeaders(headers: _*) - } - } + cacheResponse[G](lifetime, isPublic)(resp) } else resp.pure[G] } yield out } + + // Here as an optimization so we don't recreate durations + // in cacheResponse #TeamStatic + private val tenYearDuration: FiniteDuration = 315360000.seconds + + /** + * Method in order to turn a generated Response into one that + * will be appropriately cached. + * + * Note: If set to Duration.Inf, lifetime falls back to + * 10 years for support of Http1 caches. + **/ + def cacheResponse[G[_]]( + lifetime: Duration, + isPublic: Either[CacheDirective.public.type, CacheDirective.`private`] + ): PartiallyAppliedCache[G] = { + val actualLifetime = lifetime match { + case finite: FiniteDuration => finite + case _ => tenYearDuration + // Http1 caches do not respect max-age headers, so to work globally it is recommended + // to explicitly set an Expire which requires some time interval to work + } + new PartiallyAppliedCache[G] { + override def apply[F[_]]( + resp: Response[F])(implicit M: MonadError[G, Throwable], C: Clock[G]): G[Response[F]] = + for { + now <- HttpDate.current[G] + expires <- HttpDate + .fromEpochSecond(now.epochSecond + actualLifetime.toSeconds) + .liftTo[G] + } yield { + val headers = List( + `Cache-Control`( + NonEmptyList.of( + isPublic.fold[CacheDirective](identity, identity), + CacheDirective.`max-age`(actualLifetime) + )), + HDate(now), + Expires(expires) + ) + resp.putHeaders(headers: _*) + } + + } } + + trait PartiallyAppliedCache[G[_]] { + def apply[F[_]]( + resp: Response[F])(implicit M: MonadError[G, Throwable], C: Clock[G]): G[Response[F]] + } + + trait PartiallyAppliedNoStoreCache[G[_]] { + def apply[F[_]](resp: Response[F])(implicit M: Monad[G], C: Clock[G]): G[Response[F]] + } + } From d11b41531a78014a4b7126750d230b0aad25f223 Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Thu, 13 Feb 2020 10:53:57 -0800 Subject: [PATCH 2/2] scalafmt --- .../src/main/scala/org/http4s/server/middleware/Caching.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/Caching.scala b/server/src/main/scala/org/http4s/server/middleware/Caching.scala index 28dc4cff8f2..552168a7a9e 100644 --- a/server/src/main/scala/org/http4s/server/middleware/Caching.scala +++ b/server/src/main/scala/org/http4s/server/middleware/Caching.scala @@ -91,7 +91,7 @@ object Caching { /** * Publicly Cache a Response for the given lifetime. - * + * * Note: If set to Duration.Inf, lifetime falls back to * 10 years for support of Http1 caches. **/ @@ -117,7 +117,7 @@ object Caching { /** * Privately Caches A Response for the given lifetime. - * + * * Note: If set to Duration.Inf, lifetime falls back to * 10 years for support of Http1 caches. **/