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