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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public void testSuitableForHEADIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethod() {
437 final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
438 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
439 final Header[] headers = {
440
441 };
442 entry = makeEntry(Method.GET, "/foo",
443 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
444 responseCacheControl = ResponseCacheControl.builder()
445 .setMaxAge(3600)
446 .build();
447
448 Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
449 }
450
451 @Test
452 public void testNotSuitableIfGetRequestWithHeadCacheEntry() {
453
454 entry = makeEntry(Method.HEAD, "/foo",
455 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
456 responseCacheControl = ResponseCacheControl.builder()
457 .setMaxAge(3600)
458 .build();
459
460 Assertions.assertEquals(CacheSuitability.MISMATCH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
461 }
462
463 @Test
464 public void testSuitableIfErrorRequestCacheControl() {
465
466 entry = makeEntry(Method.GET, "/foo",
467 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
468 responseCacheControl = ResponseCacheControl.builder()
469 .setMaxAge(5)
470 .build();
471
472
473
474 requestCacheControl = RequestCacheControl.builder()
475 .setStaleIfError(10)
476 .build();
477 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
478
479 requestCacheControl = RequestCacheControl.builder()
480 .setStaleIfError(5)
481 .build();
482 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
483
484 requestCacheControl = RequestCacheControl.builder()
485 .setStaleIfError(10)
486 .setMinFresh(4)
487 .build();
488 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
489
490 requestCacheControl = RequestCacheControl.builder()
491 .setStaleIfError(-1)
492 .build();
493 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
494 }
495
496 @Test
497 public void testSuitableIfErrorResponseCacheControl() {
498
499 entry = makeEntry(Method.GET, "/foo",
500 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
501 responseCacheControl = ResponseCacheControl.builder()
502 .setMaxAge(5)
503 .setStaleIfError(10)
504 .build();
505
506
507
508 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
509
510 responseCacheControl = ResponseCacheControl.builder()
511 .setMaxAge(5)
512 .setStaleIfError(5)
513 .build();
514 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
515
516 responseCacheControl = ResponseCacheControl.builder()
517 .setMaxAge(5)
518 .setStaleIfError(-1)
519 .build();
520 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
521 }
522
523 @Test
524 public void testSuitableIfErrorRequestCacheControlTakesPrecedenceOverResponseCacheControl() {
525
526 entry = makeEntry(Method.GET, "/foo",
527 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
528 responseCacheControl = ResponseCacheControl.builder()
529 .setMaxAge(5)
530 .setStaleIfError(5)
531 .build();
532
533
534
535 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
536
537 requestCacheControl = RequestCacheControl.builder()
538 .setStaleIfError(10)
539 .build();
540 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
541 }
542
543 @Test
544 public void testSuitableIfErrorConfigDefault() {
545
546 entry = makeEntry(Method.GET, "/foo",
547 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
548 responseCacheControl = ResponseCacheControl.builder()
549 .setMaxAge(5)
550 .build();
551 impl = new CachedResponseSuitabilityChecker(CacheConfig.custom()
552 .setStaleIfErrorEnabled(true)
553 .build());
554 Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
555
556 requestCacheControl = RequestCacheControl.builder()
557 .setStaleIfError(5)
558 .build();
559
560 Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
561 }
562
563 }