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 package org.apache.hc.client5.http.impl.cache;
28
29 import java.time.Duration;
30 import java.time.Instant;
31 import java.util.Iterator;
32
33 import org.apache.hc.client5.http.cache.ResponseCacheControl;
34 import org.apache.hc.client5.http.utils.DateUtils;
35 import org.apache.hc.core5.http.HttpHeaders;
36 import org.apache.hc.core5.http.HttpRequest;
37 import org.apache.hc.core5.http.HttpResponse;
38 import org.apache.hc.core5.http.HttpStatus;
39 import org.apache.hc.core5.http.HttpVersion;
40 import org.apache.hc.core5.http.Method;
41 import org.apache.hc.core5.http.ProtocolVersion;
42 import org.apache.hc.core5.http.message.MessageSupport;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 class ResponseCachingPolicy {
47
48
49
50
51
52
53
54
55
56
57
58 private static final Duration DEFAULT_FRESHNESS_DURATION = Duration.ofMinutes(5);
59
60 private static final Logger LOG = LoggerFactory.getLogger(ResponseCachingPolicy.class);
61
62 private final boolean sharedCache;
63 private final boolean neverCache1_0ResponsesWithQueryString;
64 private final boolean neverCache1_1ResponsesWithQueryString;
65
66
67
68
69
70
71
72
73
74
75
76
77 public ResponseCachingPolicy(
78 final boolean sharedCache,
79 final boolean neverCache1_0ResponsesWithQueryString,
80 final boolean neverCache1_1ResponsesWithQueryString) {
81 this.sharedCache = sharedCache;
82 this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
83 this.neverCache1_1ResponsesWithQueryString = neverCache1_1ResponsesWithQueryString;
84 }
85
86
87
88
89
90
91
92 public boolean isResponseCacheable(final ResponseCacheControl cacheControl, final HttpRequest request, final HttpResponse response) {
93 final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.DEFAULT;
94 if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) {
95 if (LOG.isDebugEnabled()) {
96 LOG.debug("Protocol version {} is non-cacheable", version);
97 }
98 return false;
99 }
100
101
102 final String httpMethod = request.getMethod();
103 if (!Method.GET.isSame(httpMethod) && !Method.HEAD.isSame(httpMethod)) {
104 if (LOG.isDebugEnabled()) {
105 LOG.debug("{} method response is not cacheable", httpMethod);
106 }
107 return false;
108 }
109
110 final int code = response.getCode();
111
112
113 if (code <= HttpStatus.SC_INFORMATIONAL) {
114 return false;
115 }
116
117 if (isKnownNonCacheableStatusCode(code)) {
118 if (LOG.isDebugEnabled()) {
119 LOG.debug("{} response is not cacheable", code);
120 }
121 return false;
122 }
123
124 if (request.getPath().contains("?")) {
125 if (neverCache1_0ResponsesWithQueryString && from1_0Origin(response)) {
126 LOG.debug("Response is not cacheable as it had a query string");
127 return false;
128 } else if (!neverCache1_1ResponsesWithQueryString && !isExplicitlyCacheable(cacheControl, response)) {
129 LOG.debug("Response is not cacheable as it is missing explicit caching headers");
130 return false;
131 }
132 }
133
134 if (cacheControl.isMustUnderstand() && !understoodStatusCode(code)) {
135
136 LOG.debug("Response contains a status code that the cache does not understand, so it's not cacheable");
137 return false;
138 }
139
140 if (isExplicitlyNonCacheable(cacheControl)) {
141 LOG.debug("Response is explicitly non-cacheable per cache control directive");
142 return false;
143 }
144
145 if (sharedCache) {
146 if (request.containsHeader(HttpHeaders.AUTHORIZATION) &&
147 cacheControl.getSharedMaxAge() == -1 &&
148 !cacheControl.isPublic()) {
149 LOG.debug("Request contains private credentials");
150 return false;
151 }
152 }
153
154
155 if (response.countHeaders(HttpHeaders.EXPIRES) > 1) {
156 LOG.debug("Multiple Expires headers");
157 return false;
158 }
159
160 if (response.countHeaders(HttpHeaders.DATE) > 1) {
161 LOG.debug("Multiple Date headers");
162 return false;
163 }
164
165 final Instant responseDate = DateUtils.parseStandardDate(response, HttpHeaders.DATE);
166 final Instant responseExpires = DateUtils.parseStandardDate(response, HttpHeaders.EXPIRES);
167
168 if (expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(cacheControl, responseDate, responseExpires)) {
169 LOG.debug("Expires header less or equal to Date header and no cache control directives");
170 return false;
171 }
172
173
174 final Iterator<String> it = MessageSupport.iterateTokens(response, HttpHeaders.VARY);
175 while (it.hasNext()) {
176 final String token = it.next();
177 if ("*".equals(token)) {
178 if (LOG.isDebugEnabled()) {
179 LOG.debug("Vary: * found");
180 }
181 return false;
182 }
183 }
184
185 return isExplicitlyCacheable(cacheControl, response) || isHeuristicallyCacheable(cacheControl, code, responseDate, responseExpires);
186 }
187
188 private static boolean isKnownCacheableStatusCode(final int status) {
189 return status == HttpStatus.SC_OK ||
190 status == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION ||
191 status == HttpStatus.SC_MULTIPLE_CHOICES ||
192 status == HttpStatus.SC_MOVED_PERMANENTLY ||
193 status == HttpStatus.SC_GONE;
194 }
195
196 private static boolean isKnownNonCacheableStatusCode(final int status) {
197 return status == HttpStatus.SC_PARTIAL_CONTENT;
198 }
199
200 private static boolean isUnknownStatusCode(final int status) {
201 if (status >= 100 && status <= 101) {
202 return false;
203 }
204 if (status >= 200 && status <= 206) {
205 return false;
206 }
207 if (status >= 300 && status <= 307) {
208 return false;
209 }
210 if (status >= 400 && status <= 417) {
211 return false;
212 }
213 return status < 500 || status > 505;
214 }
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231 protected boolean isExplicitlyNonCacheable(final ResponseCacheControl cacheControl) {
232 if (cacheControl == null) {
233 return false;
234 } else {
235
236
237
238 return cacheControl.isNoStore() || (sharedCache && cacheControl.isCachePrivate());
239 }
240 }
241
242 protected boolean isExplicitlyCacheable(final ResponseCacheControl cacheControl, final HttpResponse response) {
243 if (cacheControl.isPublic()) {
244 return true;
245 }
246 if (!sharedCache && cacheControl.isCachePrivate()) {
247 return true;
248 }
249 if (response.containsHeader(HttpHeaders.EXPIRES)) {
250 return true;
251 }
252 if (cacheControl.getMaxAge() > 0) {
253 return true;
254 }
255 if (sharedCache && cacheControl.getSharedMaxAge() > 0) {
256 return true;
257 }
258 return false;
259 }
260
261 protected boolean isHeuristicallyCacheable(final ResponseCacheControl cacheControl,
262 final int status,
263 final Instant responseDate,
264 final Instant responseExpires) {
265 if (isKnownCacheableStatusCode(status)) {
266 final Duration freshnessLifetime = calculateFreshnessLifetime(cacheControl, responseDate, responseExpires);
267
268 if (freshnessLifetime.isNegative()) {
269 if (LOG.isDebugEnabled()) {
270 LOG.debug("Freshness lifetime is invalid");
271 }
272 return false;
273 }
274
275
276 if (cacheControl.isImmutable() && responseIsStillFresh(responseDate, freshnessLifetime)) {
277 if (LOG.isDebugEnabled()) {
278 LOG.debug("Response is immutable and fresh, considered cacheable without further validation");
279 }
280 return true;
281 }
282 if (freshnessLifetime.compareTo(Duration.ZERO) > 0) {
283 return true;
284 }
285 } else if (isUnknownStatusCode(status)) {
286
287
288 if (LOG.isDebugEnabled()) {
289 LOG.debug("{} response is unknown", status);
290 }
291 return false;
292 }
293 return false;
294 }
295
296 private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final ResponseCacheControl cacheControl, final Instant responseDate, final Instant expires) {
297 if (!cacheControl.isUndefined()) {
298 return false;
299 }
300 if (expires == null || responseDate == null) {
301 return false;
302 }
303 return expires.compareTo(responseDate) <= 0;
304 }
305
306 private boolean from1_0Origin(final HttpResponse response) {
307 final Iterator<String> it = MessageSupport.iterateTokens(response, HttpHeaders.VIA);
308 if (it.hasNext()) {
309 final String token = it.next();
310 return token.startsWith("1.0 ") || token.startsWith("HTTP/1.0 ");
311 }
312 final ProtocolVersion version = response.getVersion() != null ? response.getVersion() : HttpVersion.DEFAULT;
313 return HttpVersion.HTTP_1_0.equals(version);
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342 private Duration calculateFreshnessLifetime(final ResponseCacheControl cacheControl, final Instant responseDate, final Instant responseExpires) {
343
344 if (cacheControl.isUndefined()) {
345
346 return DEFAULT_FRESHNESS_DURATION;
347 }
348
349
350 if (cacheControl.getSharedMaxAge() != -1) {
351 return Duration.ofSeconds(cacheControl.getSharedMaxAge());
352 } else if (cacheControl.getMaxAge() != -1) {
353 return Duration.ofSeconds(cacheControl.getMaxAge());
354 }
355
356 if (responseDate != null && responseExpires != null) {
357 return Duration.ofSeconds(responseExpires.getEpochSecond() - responseDate.getEpochSecond());
358 }
359
360
361 return DEFAULT_FRESHNESS_DURATION;
362 }
363
364
365
366
367
368
369
370
371
372
373
374 private boolean understoodStatusCode(final int status) {
375 return (status >= 200 && status <= 206) ||
376 (status >= 300 && status <= 399) ||
377 (status >= 400 && status <= 417) ||
378 (status == 421) ||
379 (status >= 500 && status <= 505);
380 }
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398 private boolean responseIsStillFresh(final Instant responseDate, final Duration freshnessLifetime) {
399 if (responseDate == null) {
400
401 return false;
402 }
403 final Duration age = Duration.between(responseDate, Instant.now());
404 return age.compareTo(freshnessLifetime) < 0;
405 }
406
407 }