Skip to content

Commit

Permalink
Backport http4s#3196: Add Caching Convenience Functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherDavenport authored and rossabaker committed Mar 25, 2020
1 parent 33203ce commit ed177d4
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 40 deletions.
142 changes: 102 additions & 40 deletions server/src/main/scala/org/http4s/server/middleware/Caching.scala
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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]]
}

}
8 changes: 8 additions & 0 deletions website/src/hugo/content/changelog.md
Expand Up @@ -8,6 +8,14 @@ Maintenance branches are merged before each new release. This change log is
ordered chronologically, so each release contains all changes described below
it.

# v0.21.3 (2020-03-25)

This release is fully backward compatible with 0.21.2.

## Enhancements

* [#3196](https://github.com/http4s/http4s/pull/3196): Add convenience functions to `Caching` middleware.

# v0.20.21 (2020-03-25)

This release is fully backward compatible with 0.20.20.
Expand Down

0 comments on commit ed177d4

Please sign in to comment.