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 static org.junit.Assert.assertEquals;
30 import static org.junit.Assert.assertFalse;
31 import static org.junit.Assert.assertTrue;
32
33 import java.io.ByteArrayInputStream;
34 import java.util.Date;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledThreadPoolExecutor;
37
38 import org.apache.hc.client5.http.impl.schedule.ImmediateSchedulingStrategy;
39 import org.apache.hc.client5.http.utils.DateUtils;
40 import org.apache.hc.core5.http.ClassicHttpRequest;
41 import org.apache.hc.core5.http.ClassicHttpResponse;
42 import org.apache.hc.core5.http.Header;
43 import org.apache.hc.core5.http.HttpEntity;
44 import org.apache.hc.core5.http.HttpStatus;
45 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
46 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
47 import org.easymock.EasyMock;
48 import org.junit.After;
49 import org.junit.Before;
50 import org.junit.Test;
51
52
53
54
55
56
57 public class TestRFC5861Compliance extends AbstractProtocolTest {
58
59 private ScheduledExecutorService executorService;
60
61 @Before
62 public void setup() {
63 executorService = new ScheduledThreadPoolExecutor(1);
64 EasyMock.expect(mockExecRuntime.fork(null)).andReturn(mockExecRuntime).anyTimes();
65 }
66
67 @After
68 public void cleanup() {
69 executorService.shutdownNow();
70 }
71
72 @Override
73 protected void replayMocks() {
74 super.replayMocks();
75 EasyMock.replay(mockExecRuntime);
76 }
77
78 @Override
79 protected void verifyMocks() {
80 super.verifyMocks();
81 EasyMock.verify(mockExecRuntime);
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 @Test
100 public void testStaleIfErrorInResponseIsTrueReturnsStaleEntryWithWarning()
101 throws Exception{
102 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
103 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
104 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
105 "public, max-age=5, stale-if-error=60");
106
107 backendExpectsAnyRequestAndReturn(resp1);
108
109 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
110 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
111
112 backendExpectsAnyRequestAndReturn(resp2);
113
114 replayMocks();
115 execute(req1);
116 final ClassicHttpResponse result = execute(req2);
117 verifyMocks();
118
119 HttpTestUtils.assert110WarningFound(result);
120 }
121
122 @Test
123 public void testConsumesErrorResponseWhenServingStale()
124 throws Exception{
125 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
126 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
127 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
128 "public, max-age=5, stale-if-error=60");
129
130 backendExpectsAnyRequestAndReturn(resp1);
131
132 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
133 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
134 final byte[] body101 = HttpTestUtils.getRandomBytes(101);
135 final ByteArrayInputStream buf = new ByteArrayInputStream(body101);
136 final ConsumableInputStream/cache/ConsumableInputStream.html#ConsumableInputStream">ConsumableInputStream cis = new ConsumableInputStream(buf);
137 final HttpEntity entity = new InputStreamEntity(cis, 101, null);
138 resp2.setEntity(entity);
139
140 backendExpectsAnyRequestAndReturn(resp2);
141
142 replayMocks();
143 execute(req1);
144 execute(req2);
145 verifyMocks();
146
147 assertTrue(cis.wasClosed());
148 }
149
150 @Test
151 public void testStaleIfErrorInResponseYieldsToMustRevalidate()
152 throws Exception{
153 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
154 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
155 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
156 "public, max-age=5, stale-if-error=60, must-revalidate");
157
158 backendExpectsAnyRequestAndReturn(resp1);
159
160 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
161 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
162
163 backendExpectsAnyRequestAndReturn(resp2);
164
165 replayMocks();
166 execute(req1);
167 final ClassicHttpResponse result = execute(req2);
168 verifyMocks();
169
170 assertTrue(HttpStatus.SC_OK != result.getCode());
171 }
172
173 @Test
174 public void testStaleIfErrorInResponseYieldsToProxyRevalidateForSharedCache()
175 throws Exception{
176 assertTrue(config.isSharedCache());
177 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
178 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
179 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
180 "public, max-age=5, stale-if-error=60, proxy-revalidate");
181
182 backendExpectsAnyRequestAndReturn(resp1);
183
184 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
185 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
186
187 backendExpectsAnyRequestAndReturn(resp2);
188
189 replayMocks();
190 execute(req1);
191 final ClassicHttpResponse result = execute(req2);
192 verifyMocks();
193
194 assertTrue(HttpStatus.SC_OK != result.getCode());
195 }
196
197 @Test
198 public void testStaleIfErrorInResponseNeedNotYieldToProxyRevalidateForPrivateCache()
199 throws Exception{
200 final CacheConfig configUnshared = CacheConfig.custom()
201 .setSharedCache(false).build();
202 impl = new CachingExec(new BasicHttpCache(configUnshared), null, configUnshared);
203
204 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
205 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
206 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
207 "public, max-age=5, stale-if-error=60, proxy-revalidate");
208
209 backendExpectsAnyRequestAndReturn(resp1);
210
211 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
212 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
213
214 backendExpectsAnyRequestAndReturn(resp2);
215
216 replayMocks();
217 execute(req1);
218 final ClassicHttpResponse result = execute(req2);
219 verifyMocks();
220
221 HttpTestUtils.assert110WarningFound(result);
222 }
223
224 @Test
225 public void testStaleIfErrorInResponseYieldsToExplicitFreshnessRequest()
226 throws Exception{
227 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
228 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
229 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
230 "public, max-age=5, stale-if-error=60");
231
232 backendExpectsAnyRequestAndReturn(resp1);
233
234 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
235 req2.setHeader("Cache-Control","min-fresh=2");
236 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
237
238 backendExpectsAnyRequestAndReturn(resp2);
239
240 replayMocks();
241 execute(req1);
242 final ClassicHttpResponse result = execute(req2);
243 verifyMocks();
244
245 assertTrue(HttpStatus.SC_OK != result.getCode());
246 }
247
248 @Test
249 public void testStaleIfErrorInRequestIsTrueReturnsStaleEntryWithWarning()
250 throws Exception{
251 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
252 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
253 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
254 "public, max-age=5");
255
256 backendExpectsAnyRequestAndReturn(resp1);
257
258 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
259 req2.setHeader("Cache-Control","public, stale-if-error=60");
260 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
261
262 backendExpectsAnyRequestAndReturn(resp2);
263
264 replayMocks();
265 execute(req1);
266 final ClassicHttpResponse result = execute(req2);
267 verifyMocks();
268
269 HttpTestUtils.assert110WarningFound(result);
270 }
271
272 @Test
273 public void testStaleIfErrorInRequestIsTrueReturnsStaleNonRevalidatableEntryWithWarning()
274 throws Exception {
275 final Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L);
276 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
277 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
278 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
279 resp1.setHeader("Cache-Control", "public, max-age=5");
280
281 backendExpectsAnyRequestAndReturn(resp1);
282
283 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
284 req2.setHeader("Cache-Control", "public, stale-if-error=60");
285 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
286
287 backendExpectsAnyRequestAndReturn(resp2);
288
289 replayMocks();
290 execute(req1);
291 final ClassicHttpResponse result = execute(req2);
292 verifyMocks();
293
294 HttpTestUtils.assert110WarningFound(result);
295 }
296
297 @Test
298 public void testStaleIfErrorInResponseIsFalseReturnsError()
299 throws Exception{
300 final Date now = new Date();
301 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
302 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
303 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
304 "public, max-age=5, stale-if-error=2");
305
306 backendExpectsAnyRequestAndReturn(resp1);
307
308 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
309 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
310
311 backendExpectsAnyRequestAndReturn(resp2);
312
313 replayMocks();
314 execute(req1);
315 final ClassicHttpResponse result = execute(req2);
316 verifyMocks();
317
318 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
319 result.getCode());
320 }
321
322 @Test
323 public void testStaleIfErrorInRequestIsFalseReturnsError()
324 throws Exception{
325 final Date now = new Date();
326 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
327 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
328 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
329 "public, max-age=5");
330
331 backendExpectsAnyRequestAndReturn(resp1);
332
333 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
334 req2.setHeader("Cache-Control","stale-if-error=2");
335 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
336
337 backendExpectsAnyRequestAndReturn(resp2);
338
339 replayMocks();
340 execute(req1);
341 final ClassicHttpResponse result = execute(req2);
342 verifyMocks();
343
344 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
345 result.getCode());
346 }
347
348
349
350
351
352
353
354
355
356 @Test
357 public void testStaleWhileRevalidateReturnsStaleEntryWithWarning()
358 throws Exception {
359 config = CacheConfig.custom()
360 .setMaxCacheEntries(MAX_ENTRIES)
361 .setMaxObjectSize(MAX_BYTES)
362 .setAsynchronousWorkers(1)
363 .build();
364
365 impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
366
367 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
368 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
369 final Date now = new Date();
370 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
371 resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
372 resp1.setHeader("ETag","\"etag\"");
373 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
374
375 backendExpectsAnyRequestAndReturn(resp1).times(1,2);
376
377 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
378
379 replayMocks();
380 execute(req1);
381 final ClassicHttpResponse result = execute(req2);
382 verifyMocks();
383
384 assertEquals(HttpStatus.SC_OK, result.getCode());
385 boolean warning110Found = false;
386 for(final Header h : result.getHeaders("Warning")) {
387 for(final WarningValue wv : WarningValue.getWarningValues(h)) {
388 if (wv.getWarnCode() == 110) {
389 warning110Found = true;
390 break;
391 }
392 }
393 }
394 assertTrue(warning110Found);
395 }
396
397 @Test
398 public void testStaleWhileRevalidateReturnsStaleNonRevalidatableEntryWithWarning()
399 throws Exception {
400 config = CacheConfig.custom().setMaxCacheEntries(MAX_ENTRIES).setMaxObjectSize(MAX_BYTES)
401 .setAsynchronousWorkers(1).build();
402
403 impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
404
405 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
406 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
407 final Date now = new Date();
408 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
409 resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
410 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
411
412 backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
413
414 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
415
416 replayMocks();
417 execute(req1);
418 final ClassicHttpResponse result = execute(req2);
419 verifyMocks();
420
421 assertEquals(HttpStatus.SC_OK, result.getCode());
422 boolean warning110Found = false;
423 for (final Header h : result.getHeaders("Warning")) {
424 for (final WarningValue wv : WarningValue.getWarningValues(h)) {
425 if (wv.getWarnCode() == 110) {
426 warning110Found = true;
427 break;
428 }
429 }
430 }
431 assertTrue(warning110Found);
432 }
433
434 @Test
435 public void testCanAlsoServeStale304sWhileRevalidating()
436 throws Exception {
437
438 config = CacheConfig.custom()
439 .setMaxCacheEntries(MAX_ENTRIES)
440 .setMaxObjectSize(MAX_BYTES)
441 .setAsynchronousWorkers(1)
442 .setSharedCache(false)
443 .build();
444 impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
445
446 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
447 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
448 final Date now = new Date();
449 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
450 resp1.setHeader("Cache-Control", "private, stale-while-revalidate=15");
451 resp1.setHeader("ETag","\"etag\"");
452 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
453
454 backendExpectsAnyRequestAndReturn(resp1).times(1,2);
455
456 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
457 req2.setHeader("If-None-Match","\"etag\"");
458
459 replayMocks();
460 execute(req1);
461 final ClassicHttpResponse result = execute(req2);
462 verifyMocks();
463
464 assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
465 boolean warning110Found = false;
466 for(final Header h : result.getHeaders("Warning")) {
467 for(final WarningValue wv : WarningValue.getWarningValues(h)) {
468 if (wv.getWarnCode() == 110) {
469 warning110Found = true;
470 break;
471 }
472 }
473 }
474 assertTrue(warning110Found);
475 }
476
477
478 @Test
479 public void testStaleWhileRevalidateYieldsToMustRevalidate()
480 throws Exception {
481
482 final Date now = new Date();
483 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
484
485 config = CacheConfig.custom()
486 .setMaxCacheEntries(MAX_ENTRIES)
487 .setMaxObjectSize(MAX_BYTES)
488 .setAsynchronousWorkers(1)
489 .build();
490 impl = new CachingExec(cache, null, config);
491
492 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
493 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
494 resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, must-revalidate");
495 resp1.setHeader("ETag","\"etag\"");
496 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
497
498 backendExpectsAnyRequestAndReturn(resp1);
499
500 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
501 final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
502 resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, must-revalidate");
503 resp2.setHeader("ETag","\"etag\"");
504 resp2.setHeader("Date", DateUtils.formatDate(now));
505
506 backendExpectsAnyRequestAndReturn(resp2);
507
508 replayMocks();
509 execute(req1);
510 final ClassicHttpResponse result = execute(req2);
511 verifyMocks();
512
513 assertEquals(HttpStatus.SC_OK, result.getCode());
514 boolean warning110Found = false;
515 for(final Header h : result.getHeaders("Warning")) {
516 for(final WarningValue wv : WarningValue.getWarningValues(h)) {
517 if (wv.getWarnCode() == 110) {
518 warning110Found = true;
519 break;
520 }
521 }
522 }
523 assertFalse(warning110Found);
524 }
525
526 @Test
527 public void testStaleWhileRevalidateYieldsToProxyRevalidateForSharedCache()
528 throws Exception {
529
530 final Date now = new Date();
531 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
532
533 config = CacheConfig.custom()
534 .setMaxCacheEntries(MAX_ENTRIES)
535 .setMaxObjectSize(MAX_BYTES)
536 .setAsynchronousWorkers(1)
537 .setSharedCache(true)
538 .build();
539 impl = new CachingExec(cache, null, config);
540
541 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
542 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
543 resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, proxy-revalidate");
544 resp1.setHeader("ETag","\"etag\"");
545 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
546
547 backendExpectsAnyRequestAndReturn(resp1);
548
549 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
550 final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
551 resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, proxy-revalidate");
552 resp2.setHeader("ETag","\"etag\"");
553 resp2.setHeader("Date", DateUtils.formatDate(now));
554
555 backendExpectsAnyRequestAndReturn(resp2);
556
557 replayMocks();
558 execute(req1);
559 final ClassicHttpResponse result = execute(req2);
560 verifyMocks();
561
562 assertEquals(HttpStatus.SC_OK, result.getCode());
563 boolean warning110Found = false;
564 for(final Header h : result.getHeaders("Warning")) {
565 for(final WarningValue wv : WarningValue.getWarningValues(h)) {
566 if (wv.getWarnCode() == 110) {
567 warning110Found = true;
568 break;
569 }
570 }
571 }
572 assertFalse(warning110Found);
573 }
574
575 @Test
576 public void testStaleWhileRevalidateYieldsToExplicitFreshnessRequest()
577 throws Exception {
578
579 final Date now = new Date();
580 final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
581
582 config = CacheConfig.custom()
583 .setMaxCacheEntries(MAX_ENTRIES)
584 .setMaxObjectSize(MAX_BYTES)
585 .setAsynchronousWorkers(1)
586 .setSharedCache(true)
587 .build();
588 impl = new CachingExec(cache, null, config);
589
590 final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
591 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
592 resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
593 resp1.setHeader("ETag","\"etag\"");
594 resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
595
596 backendExpectsAnyRequestAndReturn(resp1);
597
598 final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
599 req2.setHeader("Cache-Control","min-fresh=2");
600 final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
601 resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
602 resp2.setHeader("ETag","\"etag\"");
603 resp2.setHeader("Date", DateUtils.formatDate(now));
604
605 backendExpectsAnyRequestAndReturn(resp2);
606
607 replayMocks();
608 execute(req1);
609 final ClassicHttpResponse result = execute(req2);
610 verifyMocks();
611
612 assertEquals(HttpStatus.SC_OK, result.getCode());
613 boolean warning110Found = false;
614 for(final Header h : result.getHeaders("Warning")) {
615 for(final WarningValue wv : WarningValue.getWarningValues(h)) {
616 if (wv.getWarnCode() == 110) {
617 warning110Found = true;
618 break;
619 }
620 }
621 }
622 assertFalse(warning110Found);
623 }
624
625 }