View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  
30  import static org.junit.jupiter.api.Assertions.assertEquals;
31  import static org.junit.jupiter.api.Assertions.assertFalse;
32  import static org.junit.jupiter.api.Assertions.assertTrue;
33  
34  import java.io.ByteArrayInputStream;
35  import java.io.IOException;
36  import java.time.Instant;
37  import java.util.concurrent.ScheduledExecutorService;
38  import java.util.concurrent.ScheduledThreadPoolExecutor;
39  
40  import org.apache.hc.client5.http.HttpRoute;
41  import org.apache.hc.client5.http.classic.ExecChain;
42  import org.apache.hc.client5.http.classic.ExecRuntime;
43  import org.apache.hc.client5.http.impl.schedule.ImmediateSchedulingStrategy;
44  import org.apache.hc.client5.http.protocol.HttpClientContext;
45  import org.apache.hc.client5.http.utils.DateUtils;
46  import org.apache.hc.core5.http.ClassicHttpRequest;
47  import org.apache.hc.core5.http.ClassicHttpResponse;
48  import org.apache.hc.core5.http.Header;
49  import org.apache.hc.core5.http.HttpEntity;
50  import org.apache.hc.core5.http.HttpException;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpStatus;
53  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
54  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
55  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
56  import org.junit.jupiter.api.AfterEach;
57  import org.junit.jupiter.api.BeforeEach;
58  import org.junit.jupiter.api.Test;
59  import org.mockito.Mock;
60  import org.mockito.Mockito;
61  import org.mockito.MockitoAnnotations;
62  
63  /**
64   * A suite of acceptance tests for compliance with RFC5861, which
65   * describes the stale-if-error and stale-while-revalidate
66   * Cache-Control extensions.
67   */
68  public class TestRFC5861Compliance {
69  
70      static final int MAX_BYTES = 1024;
71      static final int MAX_ENTRIES = 100;
72      static final int ENTITY_LENGTH = 128;
73  
74      HttpHost host;
75      HttpRoute route;
76      HttpEntity body;
77      HttpClientContext context;
78      @Mock
79      ExecChain mockExecChain;
80      @Mock
81      ExecRuntime mockExecRuntime;
82      ClassicHttpRequest request;
83      ClassicHttpResponse originResponse;
84      CacheConfig config;
85      CachingExec impl;
86      HttpCache cache;
87      ScheduledExecutorService executorService;
88  
89      @BeforeEach
90      public void setUp() throws Exception {
91          MockitoAnnotations.openMocks(this);
92  
93          host = new HttpHost("foo.example.com", 80);
94  
95          route = new HttpRoute(host);
96  
97          body = HttpTestUtils.makeBody(ENTITY_LENGTH);
98  
99          request = new BasicClassicHttpRequest("GET", "/foo");
100 
101         context = HttpClientContext.create();
102 
103         originResponse = HttpTestUtils.make200Response();
104 
105         config = CacheConfig.custom()
106                 .setMaxCacheEntries(MAX_ENTRIES)
107                 .setMaxObjectSize(MAX_BYTES)
108                 .build();
109 
110         cache = new BasicHttpCache(config);
111         impl = new CachingExec(cache, null, config);
112 
113         executorService = new ScheduledThreadPoolExecutor(1);
114 
115         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
116         Mockito.when(mockExecRuntime.fork(null)).thenReturn(mockExecRuntime);
117     }
118 
119     @AfterEach
120     public void cleanup() {
121         executorService.shutdownNow();
122     }
123 
124     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
125         return impl.execute(
126                 ClassicRequestBuilder.copy(request).build(),
127                 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
128                 mockExecChain);
129     }
130 
131     /*
132      * "The stale-if-error Cache-Control extension indicates that when an
133      * error is encountered, a cached stale response MAY be used to satisfy
134      * the request, regardless of other freshness information.When used as a
135      * request Cache-Control extension, its scope of application is the request
136      * it appears in; when used as a response Cache-Control extension, its
137      * scope is any request applicable to the cached response in which it
138      * occurs.Its value indicates the upper limit to staleness; when the cached
139      * response is more stale than the indicated amount, the cached response
140      * SHOULD NOT be used to satisfy the request, absent other information.
141      * In this context, an error is any situation that would result in a
142      * 500, 502, 503, or 504 HTTP response status code being returned."
143      *
144      * http://tools.ietf.org/html/rfc5861
145      */
146     @Test
147     public void testStaleIfErrorInResponseIsTrueReturnsStaleEntryWithWarning()
148             throws Exception{
149         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
150         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
151         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
152                 "public, max-age=5, stale-if-error=60");
153 
154         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
155         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
156 
157         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
158 
159         execute(req1);
160 
161         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
162 
163         final ClassicHttpResponse result = execute(req2);
164 
165         HttpTestUtils.assert110WarningFound(result);
166     }
167 
168     @Test
169     public void testConsumesErrorResponseWhenServingStale()
170             throws Exception{
171         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
172         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
173         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
174                 "public, max-age=5, stale-if-error=60");
175 
176         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
177         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
178         final byte[] body101 = HttpTestUtils.getRandomBytes(101);
179         final ByteArrayInputStream buf = new ByteArrayInputStream(body101);
180         final ConsumableInputStream cis = new ConsumableInputStream(buf);
181         final HttpEntity entity = new InputStreamEntity(cis, 101, null);
182         resp2.setEntity(entity);
183 
184         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
185 
186         execute(req1);
187 
188         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
189 
190         execute(req2);
191 
192         assertTrue(cis.wasClosed());
193     }
194 
195     @Test
196     public void testStaleIfErrorInResponseYieldsToMustRevalidate()
197             throws Exception{
198         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
199         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
200         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
201                 "public, max-age=5, stale-if-error=60, must-revalidate");
202 
203         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
204         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
205 
206         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
207 
208         execute(req1);
209 
210         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
211 
212         final ClassicHttpResponse result = execute(req2);
213 
214         assertTrue(HttpStatus.SC_OK != result.getCode());
215     }
216 
217     @Test
218     public void testStaleIfErrorInResponseYieldsToProxyRevalidateForSharedCache()
219             throws Exception{
220         assertTrue(config.isSharedCache());
221         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
222         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
223         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
224                 "public, max-age=5, stale-if-error=60, proxy-revalidate");
225 
226         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
227         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
228 
229         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
230 
231         execute(req1);
232 
233         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
234 
235         final ClassicHttpResponse result = execute(req2);
236 
237         assertTrue(HttpStatus.SC_OK != result.getCode());
238     }
239 
240     @Test
241     public void testStaleIfErrorInResponseNeedNotYieldToProxyRevalidateForPrivateCache()
242             throws Exception{
243         final CacheConfig configUnshared = CacheConfig.custom()
244                 .setSharedCache(false).build();
245         impl = new CachingExec(new BasicHttpCache(configUnshared), null, configUnshared);
246 
247         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
248         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
249         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
250                 "public, max-age=5, stale-if-error=60, proxy-revalidate");
251 
252         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
253         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
254 
255         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
256 
257         execute(req1);
258 
259         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
260 
261         final ClassicHttpResponse result = execute(req2);
262 
263         HttpTestUtils.assert110WarningFound(result);
264     }
265 
266     @Test
267     public void testStaleIfErrorInResponseYieldsToExplicitFreshnessRequest()
268             throws Exception{
269         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
270         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
271         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
272                 "public, max-age=5, stale-if-error=60");
273 
274         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
275         req2.setHeader("Cache-Control","min-fresh=2");
276         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
277 
278         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
279 
280         execute(req1);
281 
282         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
283 
284         final ClassicHttpResponse result = execute(req2);
285 
286         assertTrue(HttpStatus.SC_OK != result.getCode());
287     }
288 
289     @Test
290     public void testStaleIfErrorInRequestIsTrueReturnsStaleEntryWithWarning()
291             throws Exception{
292         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
293         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
294         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
295                 "public, max-age=5");
296 
297         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
298         req2.setHeader("Cache-Control","public, stale-if-error=60");
299         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
300 
301         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
302 
303         execute(req1);
304 
305         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
306 
307         final ClassicHttpResponse result = execute(req2);
308 
309         HttpTestUtils.assert110WarningFound(result);
310     }
311 
312     @Test
313     public void testStaleIfErrorInRequestIsTrueReturnsStaleNonRevalidatableEntryWithWarning()
314         throws Exception {
315         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
316         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
317         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
318         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
319         resp1.setHeader("Cache-Control", "public, max-age=5");
320 
321         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
322         req2.setHeader("Cache-Control", "public, stale-if-error=60");
323         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
324 
325         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
326 
327         execute(req1);
328 
329         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
330 
331         final ClassicHttpResponse result = execute(req2);
332 
333         HttpTestUtils.assert110WarningFound(result);
334     }
335 
336     @Test
337     public void testStaleIfErrorInResponseIsFalseReturnsError()
338             throws Exception{
339         final Instant now = Instant.now();
340         final Instant tenSecondsAgo = now.minusSeconds(10);
341         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
342         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
343                 "public, max-age=5, stale-if-error=2");
344 
345         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
346         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
347 
348         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
349 
350         execute(req1);
351 
352         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
353 
354         final ClassicHttpResponse result = execute(req2);
355 
356         assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
357                 result.getCode());
358     }
359 
360     @Test
361     public void testStaleIfErrorInRequestIsFalseReturnsError()
362             throws Exception{
363         final Instant now = Instant.now();
364         final Instant tenSecondsAgo = now.minusSeconds(10);
365         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
366         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
367                 "public, max-age=5");
368 
369         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
370         req2.setHeader("Cache-Control","stale-if-error=2");
371         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
372 
373         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
374 
375         execute(req1);
376 
377         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
378 
379         final ClassicHttpResponse result = execute(req2);
380 
381         assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.getCode());
382     }
383 
384     /*
385      * When present in an HTTP response, the stale-while-revalidate Cache-
386      * Control extension indicates that caches MAY serve the response in
387      * which it appears after it becomes stale, up to the indicated number
388      * of seconds.
389      *
390      * http://tools.ietf.org/html/rfc5861
391      */
392     @Test
393     public void testStaleWhileRevalidateReturnsStaleEntryWithWarning()
394         throws Exception {
395         config = CacheConfig.custom()
396                 .setMaxCacheEntries(MAX_ENTRIES)
397                 .setMaxObjectSize(MAX_BYTES)
398                 .setAsynchronousWorkers(1)
399                 .build();
400 
401         impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
402 
403         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
404         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
405         final Instant now = Instant.now();
406         final Instant tenSecondsAgo = now.minusSeconds(10);
407         resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
408         resp1.setHeader("ETag","\"etag\"");
409         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
410 
411         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
412 
413         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
414 
415         execute(req1);
416         final ClassicHttpResponse result = execute(req2);
417 
418         assertEquals(HttpStatus.SC_OK, result.getCode());
419         boolean warning110Found = false;
420         for(final Header h : result.getHeaders("Warning")) {
421             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
422                 if (wv.getWarnCode() == 110) {
423                     warning110Found = true;
424                     break;
425                 }
426             }
427         }
428         assertTrue(warning110Found);
429 
430         Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
431         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
432     }
433 
434     @Test
435     public void testStaleWhileRevalidateReturnsStaleNonRevalidatableEntryWithWarning()
436         throws Exception {
437         config = CacheConfig.custom().setMaxCacheEntries(MAX_ENTRIES).setMaxObjectSize(MAX_BYTES)
438             .setAsynchronousWorkers(1).build();
439 
440         impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
441 
442         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
443         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
444         final Instant now = Instant.now();
445         final Instant tenSecondsAgo = now.minusSeconds(10);
446         resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
447         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
448 
449         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
450 
451         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
452 
453         execute(req1);
454         final ClassicHttpResponse result = execute(req2);
455 
456         assertEquals(HttpStatus.SC_OK, result.getCode());
457         boolean warning110Found = false;
458         for (final Header h : result.getHeaders("Warning")) {
459             for (final WarningValue wv : WarningValue.getWarningValues(h)) {
460                 if (wv.getWarnCode() == 110) {
461                     warning110Found = true;
462                     break;
463                 }
464             }
465         }
466         assertTrue(warning110Found);
467 
468         Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
469         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
470     }
471 
472     @Test
473     public void testCanAlsoServeStale304sWhileRevalidating()
474         throws Exception {
475 
476         config = CacheConfig.custom()
477                 .setMaxCacheEntries(MAX_ENTRIES)
478                 .setMaxObjectSize(MAX_BYTES)
479                 .setAsynchronousWorkers(1)
480                 .setSharedCache(false)
481                 .build();
482         impl = new CachingExec(cache, executorService, ImmediateSchedulingStrategy.INSTANCE, config);
483 
484         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
485         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
486         final Instant now = Instant.now();
487         final Instant tenSecondsAgo = now.minusSeconds(10);
488         resp1.setHeader("Cache-Control", "private, stale-while-revalidate=15");
489         resp1.setHeader("ETag","\"etag\"");
490         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
491 
492         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
493         req2.setHeader("If-None-Match","\"etag\"");
494 
495         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
496 
497         execute(req1);
498         final ClassicHttpResponse result = execute(req2);
499 
500         assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
501         boolean warning110Found = false;
502         for(final Header h : result.getHeaders("Warning")) {
503             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
504                 if (wv.getWarnCode() == 110) {
505                     warning110Found = true;
506                     break;
507                 }
508             }
509         }
510         assertTrue(warning110Found);
511 
512         Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
513         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
514     }
515 
516     @Test
517     public void testStaleWhileRevalidateYieldsToMustRevalidate()
518         throws Exception {
519 
520         final Instant now = Instant.now();
521         final Instant tenSecondsAgo = now.minusSeconds(10);
522 
523         config = CacheConfig.custom()
524                 .setMaxCacheEntries(MAX_ENTRIES)
525                 .setMaxObjectSize(MAX_BYTES)
526                 .setAsynchronousWorkers(1)
527                 .build();
528         impl = new CachingExec(cache, null, config);
529 
530         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
531         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
532         resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, must-revalidate");
533         resp1.setHeader("ETag","\"etag\"");
534         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
535 
536         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
537         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
538         resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, must-revalidate");
539         resp2.setHeader("ETag","\"etag\"");
540         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
541 
542         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
543 
544         execute(req1);
545 
546         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
547 
548         final ClassicHttpResponse result = execute(req2);
549 
550         assertEquals(HttpStatus.SC_OK, result.getCode());
551         boolean warning110Found = false;
552         for(final Header h : result.getHeaders("Warning")) {
553             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
554                 if (wv.getWarnCode() == 110) {
555                     warning110Found = true;
556                     break;
557                 }
558             }
559         }
560         assertFalse(warning110Found);
561     }
562 
563     @Test
564     public void testStaleWhileRevalidateYieldsToProxyRevalidateForSharedCache()
565         throws Exception {
566 
567         final Instant now = Instant.now();
568         final Instant tenSecondsAgo = now.minusSeconds(10);
569 
570         config = CacheConfig.custom()
571                 .setMaxCacheEntries(MAX_ENTRIES)
572                 .setMaxObjectSize(MAX_BYTES)
573                 .setAsynchronousWorkers(1)
574                 .setSharedCache(true)
575                 .build();
576         impl = new CachingExec(cache, null, config);
577 
578         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
579         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
580         resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, proxy-revalidate");
581         resp1.setHeader("ETag","\"etag\"");
582         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
583 
584         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
585         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
586         resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15, proxy-revalidate");
587         resp2.setHeader("ETag","\"etag\"");
588         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
589 
590         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
591 
592         execute(req1);
593 
594         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
595 
596         final ClassicHttpResponse result = execute(req2);
597 
598         assertEquals(HttpStatus.SC_OK, result.getCode());
599         boolean warning110Found = false;
600         for(final Header h : result.getHeaders("Warning")) {
601             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
602                 if (wv.getWarnCode() == 110) {
603                     warning110Found = true;
604                     break;
605                 }
606             }
607         }
608         assertFalse(warning110Found);
609     }
610 
611     @Test
612     public void testStaleWhileRevalidateYieldsToExplicitFreshnessRequest()
613         throws Exception {
614 
615         final Instant now = Instant.now();
616         final Instant tenSecondsAgo = now.minusSeconds(10);
617 
618         config = CacheConfig.custom()
619                 .setMaxCacheEntries(MAX_ENTRIES)
620                 .setMaxObjectSize(MAX_BYTES)
621                 .setAsynchronousWorkers(1)
622                 .setSharedCache(true)
623                 .build();
624         impl = new CachingExec(cache, null, config);
625 
626         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
627         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
628         resp1.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
629         resp1.setHeader("ETag","\"etag\"");
630         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
631 
632         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
633         req2.setHeader("Cache-Control","min-fresh=2");
634         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
635         resp2.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=15");
636         resp2.setHeader("ETag","\"etag\"");
637         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
638 
639         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
640 
641         execute(req1);
642 
643         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
644 
645         final ClassicHttpResponse result = execute(req2);
646 
647         assertEquals(HttpStatus.SC_OK, result.getCode());
648         boolean warning110Found = false;
649         for(final Header h : result.getHeaders("Warning")) {
650             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
651                 if (wv.getWarnCode() == 110) {
652                     warning110Found = true;
653                     break;
654                 }
655             }
656         }
657         assertFalse(warning110Found);
658     }
659 
660 }