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.Instant;
30
31 import org.apache.hc.client5.http.cache.HttpCacheEntry;
32 import org.apache.hc.client5.http.utils.DateUtils;
33 import org.apache.hc.core5.http.Header;
34 import org.apache.hc.core5.http.HttpHost;
35 import org.apache.hc.core5.http.HttpRequest;
36 import org.apache.hc.core5.http.Method;
37 import org.apache.hc.core5.http.message.BasicHeader;
38 import org.apache.hc.core5.http.message.BasicHttpRequest;
39 import org.apache.hc.core5.http.support.BasicRequestBuilder;
40 import org.apache.hc.core5.util.TimeValue;
41 import org.junit.jupiter.api.Assertions;
42 import org.junit.jupiter.api.BeforeEach;
43 import org.junit.jupiter.api.Test;
44
45 public class TestCachedResponseSuitabilityChecker {
46
47 private Instant now;
48 private Instant elevenSecondsAgo;
49 private Instant tenSecondsAgo;
50 private Instant nineSecondsAgo;
51
52 private HttpRequest request;
53 private HttpCacheEntry entry;
54 private RequestCacheControl requestCacheControl;
55 private ResponseCacheControl responseCacheControl;
56 private CachedResponseSuitabilityChecker impl;
57
58 @BeforeEach
59 public void setUp() {
60 now = Instant.now();
61 elevenSecondsAgo = now.minusSeconds(11);
62 tenSecondsAgo = now.minusSeconds(10);
63 nineSecondsAgo = now.minusSeconds(9);
64
65 request = new BasicHttpRequest("GET", "/foo");
66 entry = HttpTestUtils.makeCacheEntry();
67 requestCacheControl = RequestCacheControl.builder().build();
68 responseCacheControl = ResponseCacheControl.builder().build();
69
70 impl = new CachedResponseSuitabilityChecker(CacheConfig.DEFAULT);
71 }
72
73 private HttpCacheEntry makeEntry(final Instant requestDate,
74 final Instant responseDate,
75 final Method method,
76 final String requestUri,
77 final Header[] requestHeaders,
78 final int status,
79 final Header[] responseHeaders) {
80 return HttpTestUtils.makeCacheEntry(requestDate, responseDate, method, requestUri, requestHeaders,
81 status, responseHeaders, HttpTestUtils.makeNullResource());
82 }
83
84 private HttpCacheEntry makeEntry(final Header... headers) {
85 return makeEntry(elevenSecondsAgo, nineSecondsAgo, Method.GET, "/foo", null, 200, headers);
86 }
87
88 private HttpCacheEntry makeEntry(final Instant requestDate,
89 final Instant responseDate,
90 final Header... headers) {
91 return makeEntry(requestDate, responseDate, Method.GET, "/foo", null, 200, headers);
92 }
93
94 private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header... headers) {
95 return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, null, 200, headers);
96 }
97
98 private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header[] requestHeaders,
99 final int status, final Header[] responseHeaders) {
100 return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, requestHeaders,
101 status, responseHeaders);
102 }
103
104 @Test
105 public void testRequestMethodMatch() {
106 request = new BasicHttpRequest("GET", "/foo");
107 entry = makeEntry(Method.GET, "/foo",
108 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
109 Assertions.assertTrue(impl.requestMethodMatch(request, entry));
110
111 request = new BasicHttpRequest("HEAD", "/foo");
112 Assertions.assertTrue(impl.requestMethodMatch(request, entry));
113
114 request = new BasicHttpRequest("POST", "/foo");
115 Assertions.assertFalse(impl.requestMethodMatch(request, entry));
116
117 request = new BasicHttpRequest("HEAD", "/foo");
118 entry = makeEntry(Method.HEAD, "/foo",
119 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
120 Assertions.assertTrue(impl.requestMethodMatch(request, entry));
121
122 request = new BasicHttpRequest("GET", "/foo");
123 entry = makeEntry(Method.HEAD, "/foo",
124 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
125 Assertions.assertFalse(impl.requestMethodMatch(request, entry));
126 }
127
128 @Test
129 public void testRequestUriMatch() {
130 request = new BasicHttpRequest("GET", "/foo");
131 entry = makeEntry(Method.GET, "/foo",
132 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
133 Assertions.assertTrue(impl.requestUriMatch(request, entry));
134
135 request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo");
136 entry = makeEntry(Method.GET, "http://some-host:80/foo",
137 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
138 Assertions.assertTrue(impl.requestUriMatch(request, entry));
139
140 request = new BasicHttpRequest("GET", new HttpHost("Some-Host"), "/foo?bar");
141 entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
142 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
143 Assertions.assertTrue(impl.requestUriMatch(request, entry));
144
145 request = new BasicHttpRequest("GET", new HttpHost("some-other-host"), "/foo");
146 entry = makeEntry(Method.GET, "http://some-host:80/foo",
147 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
148 Assertions.assertFalse(impl.requestUriMatch(request, entry));
149
150 request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo?huh");
151 entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
152 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
153 Assertions.assertFalse(impl.requestUriMatch(request, entry));
154 }
155
156 @Test
157 public void testRequestHeadersMatch() {
158 request = BasicRequestBuilder.get("/foo").build();
159 entry = makeEntry(
160 Method.GET, "/foo",
161 new Header[]{},
162 200,
163 new Header[]{
164 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
165 });
166 Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
167
168 request = BasicRequestBuilder.get("/foo").build();
169 entry = makeEntry(
170 Method.GET, "/foo",
171 new Header[]{},
172 200,
173 new Header[]{
174 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
175 new BasicHeader("Vary", "")
176 });
177 Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
178
179 request = BasicRequestBuilder.get("/foo")
180 .addHeader("Accept-Encoding", "blah")
181 .build();
182 entry = makeEntry(
183 Method.GET, "/foo",
184 new Header[]{
185 new BasicHeader("Accept-Encoding", "blah")
186 },
187 200,
188 new Header[]{
189 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
190 new BasicHeader("Vary", "Accept-Encoding")
191 });
192 Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
193
194 request = BasicRequestBuilder.get("/foo")
195 .addHeader("Accept-Encoding", "gzip, deflate, deflate , zip, ")
196 .build();
197 entry = makeEntry(
198 Method.GET, "/foo",
199 new Header[]{
200 new BasicHeader("Accept-Encoding", " gzip, zip, deflate")
201 },
202 200,
203 new Header[]{
204 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
205 new BasicHeader("Vary", "Accept-Encoding")
206 });
207 Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
208
209 request = BasicRequestBuilder.get("/foo")
210 .addHeader("Accept-Encoding", "gzip, deflate, zip")
211 .build();
212 entry = makeEntry(
213 Method.GET, "/foo",
214 new Header[]{
215 new BasicHeader("Accept-Encoding", " gzip, deflate")
216 },
217 200,
218 new Header[]{
219 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
220 new BasicHeader("Vary", "Accept-Encoding")
221 });
222 Assertions.assertFalse(impl.requestHeadersMatch(request, entry));
223 }
224
225 @Test
226 public void testResponseNoCache() {
227 entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
228 responseCacheControl = ResponseCacheControl.builder()
229 .setNoCache(false)
230 .build();
231
232 Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
233
234 responseCacheControl = ResponseCacheControl.builder()
235 .setNoCache(true)
236 .build();
237
238 Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
239
240 responseCacheControl = ResponseCacheControl.builder()
241 .setNoCache(true)
242 .setNoCacheFields("stuff", "more-stuff")
243 .build();
244
245 Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
246
247 entry = makeEntry(
248 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
249 new BasicHeader("stuff", "booh"));
250
251 Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
252 }
253
254 @Test
255 public void testSuitableIfCacheEntryIsFresh() {
256 entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
257 responseCacheControl = ResponseCacheControl.builder()
258 .setMaxAge(3600)
259 .build();
260 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
261 }
262
263 @Test
264 public void testNotSuitableIfCacheEntryIsNotFresh() {
265 entry = makeEntry(
266 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
267 responseCacheControl = ResponseCacheControl.builder()
268 .setMaxAge(5)
269 .build();
270 Assertions.assertEquals(CacheSuitability.STALE, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
271 }
272
273 @Test
274 public void testNotSuitableIfRequestHasNoCache() {
275 requestCacheControl = RequestCacheControl.builder()
276 .setNoCache(true)
277 .build();
278 entry = makeEntry(
279 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
280 responseCacheControl = ResponseCacheControl.builder()
281 .setMaxAge(3600)
282 .build();
283 Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
284 }
285
286 @Test
287 public void testNotSuitableIfAgeExceedsRequestMaxAge() {
288 requestCacheControl = RequestCacheControl.builder()
289 .setMaxAge(10)
290 .build();
291 responseCacheControl = ResponseCacheControl.builder()
292 .setMaxAge(3600)
293 .build();
294 entry = makeEntry(
295 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
296 Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
297 }
298
299 @Test
300 public void testSuitableIfFreshAndAgeIsUnderRequestMaxAge() {
301 requestCacheControl = RequestCacheControl.builder()
302 .setMaxAge(15)
303 .build();
304 entry = makeEntry(
305 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
306 responseCacheControl = ResponseCacheControl.builder()
307 .setMaxAge(3600)
308 .build();
309 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
310 }
311
312 @Test
313 public void testSuitableIfFreshAndFreshnessLifetimeGreaterThanRequestMinFresh() {
314 requestCacheControl = RequestCacheControl.builder()
315 .setMinFresh(10)
316 .build();
317 entry = makeEntry(
318 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
319 responseCacheControl = ResponseCacheControl.builder()
320 .setMaxAge(3600)
321 .build();
322 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
323 }
324
325 @Test
326 public void testNotSuitableIfFreshnessLifetimeLessThanRequestMinFresh() {
327 requestCacheControl = RequestCacheControl.builder()
328 .setMinFresh(10)
329 .build();
330 entry = makeEntry(
331 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
332 responseCacheControl = ResponseCacheControl.builder()
333 .setMaxAge(15)
334 .build();
335 Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
336 }
337
338 @Test
339 public void testSuitableEvenIfStaleButPermittedByRequestMaxStale() {
340 requestCacheControl = RequestCacheControl.builder()
341 .setMaxStale(10)
342 .build();
343 final Header[] headers = {
344 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
345 };
346 entry = makeEntry(headers);
347 responseCacheControl = ResponseCacheControl.builder()
348 .setMaxAge(5)
349 .build();
350 Assertions.assertEquals(CacheSuitability.FRESH_ENOUGH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
351 }
352
353 @Test
354 public void testNotSuitableIfStaleButTooStaleForRequestMaxStale() {
355 requestCacheControl = RequestCacheControl.builder()
356 .setMaxStale(2)
357 .build();
358 entry = makeEntry(
359 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
360 responseCacheControl = ResponseCacheControl.builder()
361 .setMaxAge(5)
362 .build();
363 Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
364 }
365
366 @Test
367 public void testSuitableIfCacheEntryIsHeuristicallyFreshEnough() {
368 final Instant oneSecondAgo = now.minusSeconds(1);
369 final Instant twentyOneSecondsAgo = now.minusSeconds(21);
370
371 entry = makeEntry(oneSecondAgo, oneSecondAgo,
372 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
373 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(twentyOneSecondsAgo)));
374
375 final CacheConfig config = CacheConfig.custom()
376 .setHeuristicCachingEnabled(true)
377 .setHeuristicCoefficient(0.1f).build();
378 impl = new CachedResponseSuitabilityChecker(config);
379
380 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
381 }
382
383 @Test
384 public void testSuitableIfCacheEntryIsHeuristicallyFreshEnoughByDefault() {
385 entry = makeEntry(
386 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
387
388 final CacheConfig config = CacheConfig.custom()
389 .setHeuristicCachingEnabled(true)
390 .setHeuristicDefaultLifetime(TimeValue.ofSeconds(20L))
391 .build();
392 impl = new CachedResponseSuitabilityChecker(config);
393
394 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
395 }
396
397 @Test
398 public void testSuitableIfRequestMethodisHEAD() {
399 final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
400 entry = makeEntry(
401 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
402 responseCacheControl = ResponseCacheControl.builder()
403 .setMaxAge(3600)
404 .build();
405
406 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
407 }
408
409 @Test
410 public void testSuitableForGETIfEntryDoesNotSpecifyARequestMethodButContainsEntity() {
411 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
412 entry = makeEntry(Method.GET, "/foo",
413 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
414 responseCacheControl = ResponseCacheControl.builder()
415 .setMaxAge(3600)
416 .build();
417
418 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
419 }
420
421 @Test
422 public void testSuitableForGETIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethodButContains204Response() {
423 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
424 entry = makeEntry(Method.GET, "/foo",
425 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
426 responseCacheControl = ResponseCacheControl.builder()
427 .setMaxAge(3600)
428 .build();
429
430 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
431 }
432
433 @Test
434 public void testSuitableForHEADIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethod() {
435 final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
436 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
437 final Header[] headers = {
438
439 };
440 entry = makeEntry(Method.GET, "/foo",
441 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
442 responseCacheControl = ResponseCacheControl.builder()
443 .setMaxAge(3600)
444 .build();
445
446 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
447 }
448
449 @Test
450 public void testNotSuitableIfGetRequestWithHeadCacheEntry() {
451
452 entry = makeEntry(Method.HEAD, "/foo",
453 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
454 responseCacheControl = ResponseCacheControl.builder()
455 .setMaxAge(3600)
456 .build();
457
458 Assertions.assertEquals(CacheSuitability.MISMATCH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
459 }
460
461 @Test
462 public void testSuitableIfErrorRequestCacheControl() {
463
464 entry = makeEntry(Method.GET, "/foo",
465 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
466 responseCacheControl = ResponseCacheControl.builder()
467 .setMaxAge(5)
468 .build();
469
470
471
472 requestCacheControl = RequestCacheControl.builder()
473 .setStaleIfError(10)
474 .build();
475 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
476
477 requestCacheControl = RequestCacheControl.builder()
478 .setStaleIfError(5)
479 .build();
480 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
481
482 requestCacheControl = RequestCacheControl.builder()
483 .setStaleIfError(10)
484 .setMinFresh(4)
485 .build();
486 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
487
488 requestCacheControl = RequestCacheControl.builder()
489 .setStaleIfError(-1)
490 .build();
491 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
492 }
493
494 @Test
495 public void testSuitableIfErrorResponseCacheControl() {
496
497 entry = makeEntry(Method.GET, "/foo",
498 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
499 responseCacheControl = ResponseCacheControl.builder()
500 .setMaxAge(5)
501 .setStaleIfError(10)
502 .build();
503
504
505
506 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
507
508 responseCacheControl = ResponseCacheControl.builder()
509 .setMaxAge(5)
510 .setStaleIfError(5)
511 .build();
512 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
513
514 responseCacheControl = ResponseCacheControl.builder()
515 .setMaxAge(5)
516 .setStaleIfError(-1)
517 .build();
518 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
519 }
520
521 @Test
522 public void testSuitableIfErrorRequestCacheControlTakesPrecedenceOverResponseCacheControl() {
523
524 entry = makeEntry(Method.GET, "/foo",
525 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
526 responseCacheControl = ResponseCacheControl.builder()
527 .setMaxAge(5)
528 .setStaleIfError(5)
529 .build();
530
531
532
533 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
534
535 requestCacheControl = RequestCacheControl.builder()
536 .setStaleIfError(10)
537 .build();
538 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
539 }
540
541 @Test
542 public void testSuitableIfErrorConfigDefault() {
543
544 entry = makeEntry(Method.GET, "/foo",
545 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
546 responseCacheControl = ResponseCacheControl.builder()
547 .setMaxAge(5)
548 .build();
549 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom()
550 .setStaleIfErrorEnabled(true)
551 .build());
552 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
553
554 requestCacheControl = RequestCacheControl.builder()
555 .setStaleIfError(5)
556 .build();
557
558 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
559 }
560
561 }