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