/
Caching.scala
208 lines (189 loc) · 6.91 KB
/
Caching.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/*
* Copyright 2014 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.http4s.server.middleware
import cats._
import cats.syntax.all._
import cats.effect.{MonadThrow => _, _}
import cats.data._
import org.http4s._
import org.http4s.headers.{Date => HDate, _}
import org.typelevel.ci.CIString
import scala.concurrent.duration._
/** Caching contains middlewares to support caching functionality.
*
* Helper functions to support [[Caching.cache]] can be found in
* [[Caching.Helpers]]
*/
object Caching {
/** Middleware that implies responses should NOT be cached.
* This is a best attempt, many implementors of caching have done so differently.
*/
def `no-store`[G[_]: Monad: Clock, F[_], A](
http: Kleisli[G, A, Response[F]]): Kleisli[G, A, Response[F]] =
Kleisli { (a: A) =>
for {
resp <- http(a)
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)).putHeaders(noStoreStaticHeaders))
}
// These never change, so don't recreate them each time.
private val noStoreStaticHeaders: List[Header.ToRaw] = List(
`Cache-Control`(
NonEmptyList.of[CacheDirective](
CacheDirective.`no-store`,
CacheDirective.`no-cache`(),
CacheDirective.`max-age`(0.seconds)
)
),
"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
* [[publicCache]] and [[privateCache]].
*/
object Helpers {
def defaultStatusToSetOn(s: Status): Boolean =
s match {
case Status.NotModified => true
case otherwise => otherwise.isSuccess
}
def defaultMethodsToSetOn(m: Method): Boolean = methodsToSetOn.contains(m)
private lazy val methodsToSetOn: Set[Method] = Set(
Method.GET,
Method.HEAD
)
}
/** Sets headers for response to be publicly cached for the specified duration.
*
* Note: If set to Duration.Inf, lifetime falls back to
* 10 years for support of Http1 caches.
*/
def publicCache[G[_]: MonadThrow: Clock, F[_]](lifetime: Duration, http: Http[G, F]): Http[G, F] =
cache(
lifetime,
Either.left(CacheDirective.public),
Helpers.defaultMethodsToSetOn,
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.
*
* Note: If set to Duration.Inf, lifetime falls back to
* 10 years for support of Http1 caches.
*/
def privateCache[G[_]: MonadThrow: Clock, F[_]](
lifetime: Duration,
http: Http[G, F],
fieldNames: List[CIString] = Nil): Http[G, F] =
cache(
lifetime,
Either.right(CacheDirective.`private`(fieldNames)),
Helpers.defaultMethodsToSetOn,
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[CIString] = Nil
): PartiallyAppliedCache[G] =
cacheResponse(lifetime, Either.right(CacheDirective.`private`(fieldNames)))
/** Construct a Middleware that will apply the appropriate caching headers.
*
* Helper functions for methodToSetOn and statusToSetOn can be found in [[Helpers]].
*
* Note: If set to Duration.Inf, lifetime falls back to
* 10 years for support of Http1 caches.
*/
def cache[G[_]: MonadThrow: Clock, F[_]](
lifetime: Duration,
isPublic: Either[CacheDirective.public.type, CacheDirective.`private`],
methodToSetOn: Method => Boolean,
statusToSetOn: Status => Boolean,
http: Http[G, F]
): Http[G, F] =
Kleisli { (req: Request[F]) =>
for {
resp <- http(req)
out <-
if (methodToSetOn(req.method) && statusToSetOn(resp.status))
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: MonadThrow[G], C: Clock[G]): G[Response[F]] =
for {
now <- HttpDate.current[G]
expires <-
HttpDate
.fromEpochSecond(now.epochSecond + actualLifetime.toSeconds)
.liftTo[G]
} yield resp.putHeaders(
`Cache-Control`(
NonEmptyList.of(
isPublic.fold[CacheDirective](identity, identity),
CacheDirective.`max-age`(actualLifetime)
)),
HDate(now),
Expires(expires)
)
}
}
trait PartiallyAppliedCache[G[_]] {
def apply[F[_]](resp: Response[F])(implicit M: MonadThrow[G], 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]]
}
}