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.assertNotEquals;
33  import static org.junit.jupiter.api.Assertions.assertNotNull;
34  import static org.junit.jupiter.api.Assertions.assertNull;
35  import static org.junit.jupiter.api.Assertions.assertTrue;
36  
37  import java.io.IOException;
38  import java.time.Instant;
39  import java.time.temporal.ChronoUnit;
40  import java.util.Arrays;
41  import java.util.Iterator;
42  import java.util.List;
43  
44  import org.apache.hc.client5.http.HttpRoute;
45  import org.apache.hc.client5.http.auth.StandardAuthScheme;
46  import org.apache.hc.client5.http.classic.ExecChain;
47  import org.apache.hc.client5.http.classic.ExecRuntime;
48  import org.apache.hc.client5.http.classic.methods.HttpGet;
49  import org.apache.hc.client5.http.classic.methods.HttpPost;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.utils.DateUtils;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.Header;
55  import org.apache.hc.core5.http.HeaderElement;
56  import org.apache.hc.core5.http.HttpEntity;
57  import org.apache.hc.core5.http.HttpException;
58  import org.apache.hc.core5.http.HttpHeaders;
59  import org.apache.hc.core5.http.HttpHost;
60  import org.apache.hc.core5.http.HttpStatus;
61  import org.apache.hc.core5.http.HttpVersion;
62  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
63  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
64  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
65  import org.apache.hc.core5.http.message.MessageSupport;
66  import org.junit.jupiter.api.BeforeEach;
67  import org.junit.jupiter.api.Test;
68  import org.mockito.ArgumentCaptor;
69  import org.mockito.Mock;
70  import org.mockito.Mockito;
71  import org.mockito.MockitoAnnotations;
72  
73  /*
74   * This test class captures functionality required to achieve unconditional
75   * compliance with the HTTP/1.1 spec, i.e. all the SHOULD, SHOULD NOT,
76   * RECOMMENDED, and NOT RECOMMENDED behaviors.
77   */
78  public class TestProtocolRecommendations {
79  
80      static final int MAX_BYTES = 1024;
81      static final int MAX_ENTRIES = 100;
82      static final int ENTITY_LENGTH = 128;
83  
84      HttpHost host;
85      HttpRoute route;
86      HttpEntity body;
87      HttpClientContext context;
88      @Mock
89      ExecChain mockExecChain;
90      @Mock
91      ExecRuntime mockExecRuntime;
92      ClassicHttpRequest request;
93      ClassicHttpResponse originResponse;
94      CacheConfig config;
95      CachingExec impl;
96      HttpCache cache;
97      Instant now;
98      Instant tenSecondsAgo;
99      Instant twoMinutesAgo;
100 
101     @BeforeEach
102     public void setUp() throws Exception {
103         MockitoAnnotations.openMocks(this);
104         host = new HttpHost("foo.example.com", 80);
105 
106         route = new HttpRoute(host);
107 
108         body = HttpTestUtils.makeBody(ENTITY_LENGTH);
109 
110         request = new BasicClassicHttpRequest("GET", "/foo");
111 
112         context = HttpClientContext.create();
113 
114         originResponse = HttpTestUtils.make200Response();
115 
116         config = CacheConfig.custom()
117                 .setMaxCacheEntries(MAX_ENTRIES)
118                 .setMaxObjectSize(MAX_BYTES)
119                 .build();
120 
121         cache = new BasicHttpCache(config);
122         impl = new CachingExec(cache, null, config);
123 
124         now = Instant.now();
125         tenSecondsAgo = now.minus(10, ChronoUnit.SECONDS);
126         twoMinutesAgo = now.minus(1, ChronoUnit.MINUTES);
127 
128         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
129     }
130 
131     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
132         return impl.execute(
133                 ClassicRequestBuilder.copy(request).build(),
134                 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
135                 mockExecChain);
136     }
137 
138     /* "identity: The default (identity) encoding; the use of no
139      * transformation whatsoever. This content-coding is used only in the
140      * Accept-Encoding header, and SHOULD NOT be used in the
141      * Content-Encoding header."
142      *
143      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5
144      */
145     @Test
146     public void testIdentityCodingIsNotUsedInContentEncodingHeader() throws Exception {
147         originResponse.setHeader("Content-Encoding", "identity");
148         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
149 
150         final ClassicHttpResponse result = execute(request);
151 
152         boolean foundIdentity = false;
153         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
154         while (it.hasNext()) {
155             final HeaderElement elt = it.next();
156             if ("identity".equalsIgnoreCase(elt.getName())) {
157                 foundIdentity = true;
158             }
159         }
160         assertFalse(foundIdentity);
161     }
162 
163     /*
164      * "304 Not Modified. ... If the conditional GET used a strong cache
165      * validator (see section 13.3.3), the response SHOULD NOT include
166      * other entity-headers."
167      *
168      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
169      */
170     private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
171             final String headerName, final String headerValue, final String validatorHeader,
172             final String validator, final String conditionalHeader) throws Exception {
173         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
174         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
175         resp1.setHeader("Cache-Control","max-age=3600");
176         resp1.setHeader(validatorHeader, validator);
177         resp1.setHeader(headerName, headerValue);
178 
179         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
180 
181         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
182         req2.setHeader(conditionalHeader, validator);
183 
184         execute(req1);
185         final ClassicHttpResponse result = execute(req2);
186 
187 
188         if (HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
189             assertNull(result.getFirstHeader(headerName));
190         }
191     }
192 
193     private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
194             final String headerName, final String headerValue) throws Exception {
195         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
196                 headerValue, "ETag", "\"etag\"", "If-None-Match");
197     }
198 
199     private void cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
200             final String headerName, final String headerValue) throws Exception {
201         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
202                 headerValue, "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo),
203                 "If-Modified-Since");
204     }
205 
206     @Test
207     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainAllow() throws Exception {
208         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
209                 "Allow", "GET,HEAD");
210     }
211 
212     @Test
213     public void cacheGenerated304ForStrongDateValidatorShouldNotContainAllow() throws Exception {
214         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
215                 "Allow", "GET,HEAD");
216     }
217 
218     @Test
219     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentEncoding() throws Exception {
220         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
221                 "Content-Encoding", "gzip");
222     }
223 
224     @Test
225     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentEncoding() throws Exception {
226         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
227                 "Content-Encoding", "gzip");
228     }
229 
230     @Test
231     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentLanguage() throws Exception {
232         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
233                 "Content-Language", "en");
234     }
235 
236     @Test
237     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLanguage() throws Exception {
238         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
239                 "Content-Language", "en");
240     }
241 
242     @Test
243     public void cacheGenerated304ForStrongValidatorShouldNotContainContentLength() throws Exception {
244         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
245                 "Content-Length", "128");
246     }
247 
248     @Test
249     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLength() throws Exception {
250         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
251                 "Content-Length", "128");
252     }
253 
254     @Test
255     public void cacheGenerated304ForStrongValidatorShouldNotContainContentMD5() throws Exception {
256         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
257                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
258     }
259 
260     @Test
261     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5() throws Exception {
262         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
263                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
264     }
265 
266     private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
267             final String validatorHeader, final String validator, final String conditionalHeader) throws Exception {
268         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
269         req1.setHeader("Range","bytes=0-127");
270         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
271         resp1.setHeader("Cache-Control","max-age=3600");
272         resp1.setHeader(validatorHeader, validator);
273         resp1.setHeader("Content-Range", "bytes 0-127/256");
274 
275         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
276 
277         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
278         req2.setHeader("If-Range", validator);
279         req2.setHeader("Range","bytes=0-127");
280         req2.setHeader(conditionalHeader, validator);
281 
282         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
283         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
284         resp2.setHeader(validatorHeader, validator);
285 
286         // cache module does not currently deal with byte ranges, but we want
287         // this test to work even if it does some day
288 
289         execute(req1);
290         final ClassicHttpResponse result = execute(req2);
291 
292         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
293         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
294 
295         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
296         if (allRequests.isEmpty() && HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
297             // cache generated a 304
298             assertNull(result.getFirstHeader("Content-Range"));
299         }
300     }
301 
302     @Test
303     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange() throws Exception {
304         cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
305                 "ETag", "\"etag\"", "If-None-Match");
306     }
307 
308     @Test
309     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange() throws Exception {
310         cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
311                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo), "If-Modified-Since");
312     }
313 
314     @Test
315     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType() throws Exception {
316         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
317                 "Content-Type", "text/html");
318     }
319 
320     @Test
321     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentType() throws Exception {
322         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
323                 "Content-Type", "text/html");
324     }
325 
326     @Test
327     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainLastModified() throws Exception {
328         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
329                 "Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
330     }
331 
332     @Test
333     public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified() throws Exception {
334         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
335                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
336     }
337 
338     private void shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
339             final String entityHeader, final String entityHeaderValue) throws Exception {
340         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
341         req.setHeader("If-None-Match", "\"etag\"");
342 
343         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
344         resp.setHeader("Date", DateUtils.formatStandardDate(now));
345         resp.setHeader("Etag", "\"etag\"");
346         resp.setHeader(entityHeader, entityHeaderValue);
347 
348         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
349 
350         final ClassicHttpResponse result = execute(req);
351 
352         assertNull(result.getFirstHeader(entityHeader));
353     }
354 
355     @Test
356     public void shouldStripAllowFromOrigin304ResponseToStrongValidation() throws Exception {
357         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
358                 "Allow", "GET,HEAD");
359     }
360 
361     @Test
362     public void shouldStripContentEncodingFromOrigin304ResponseToStrongValidation() throws Exception {
363         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
364                 "Content-Encoding", "gzip");
365     }
366 
367     @Test
368     public void shouldStripContentLanguageFromOrigin304ResponseToStrongValidation() throws Exception {
369         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
370                 "Content-Language", "en");
371     }
372 
373     @Test
374     public void shouldStripContentLengthFromOrigin304ResponseToStrongValidation() throws Exception {
375         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
376                 "Content-Length", "128");
377     }
378 
379     @Test
380     public void shouldStripContentMD5FromOrigin304ResponseToStrongValidation() throws Exception {
381         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
382                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
383     }
384 
385     @Test
386     public void shouldStripContentTypeFromOrigin304ResponseToStrongValidation() throws Exception {
387         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
388                 "Content-Type", "text/html;charset=utf-8");
389     }
390 
391     @Test
392     public void shouldStripContentRangeFromOrigin304ResponseToStringValidation() throws Exception {
393         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
394         req.setHeader("If-Range","\"etag\"");
395         req.setHeader("Range","bytes=0-127");
396 
397         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
398         resp.setHeader("Date", DateUtils.formatStandardDate(now));
399         resp.setHeader("ETag", "\"etag\"");
400         resp.setHeader("Content-Range", "bytes 0-127/256");
401 
402         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
403 
404         final ClassicHttpResponse result = execute(req);
405 
406         assertNull(result.getFirstHeader("Content-Range"));
407     }
408 
409     @Test
410     public void shouldStripLastModifiedFromOrigin304ResponseToStrongValidation() throws Exception {
411         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
412                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
413     }
414 
415     /*
416      * "For this reason, a cache SHOULD NOT return a stale response if the
417      * client explicitly requests a first-hand or fresh one, unless it is
418      * impossible to comply for technical or policy reasons."
419      */
420     private ClassicHttpRequest requestToPopulateStaleCacheEntry() throws Exception {
421         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
422         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
423         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
424         resp1.setHeader("Cache-Control","public,max-age=5");
425         resp1.setHeader("Etag","\"etag\"");
426 
427         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
428         return req1;
429     }
430 
431     private void testDoesNotReturnStaleResponseOnError(final ClassicHttpRequest req2) throws Exception {
432         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
433 
434         execute(req1);
435 
436         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
437 
438         ClassicHttpResponse result = null;
439         try {
440             result = execute(req2);
441         } catch (final IOException acceptable) {
442         }
443 
444         if (result != null) {
445             assertNotEquals(HttpStatus.SC_OK, result.getCode());
446         }
447     }
448 
449     @Test
450     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithCacheControl() throws Exception {
451         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
452         req.setHeader("Cache-Control","no-cache");
453         testDoesNotReturnStaleResponseOnError(req);
454     }
455 
456     @Test
457     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithPragma() throws Exception {
458         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
459         req.setHeader("Pragma","no-cache");
460         testDoesNotReturnStaleResponseOnError(req);
461     }
462 
463     @Test
464     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxAge() throws Exception {
465         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
466         req.setHeader("Cache-Control","max-age=0");
467         testDoesNotReturnStaleResponseOnError(req);
468     }
469 
470     @Test
471     public void testDoesNotReturnStaleResponseIfClientExplicitlySpecifiesLargerMaxAge() throws Exception {
472         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
473         req.setHeader("Cache-Control","max-age=20");
474         testDoesNotReturnStaleResponseOnError(req);
475     }
476 
477 
478     @Test
479     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMinFresh() throws Exception {
480         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
481         req.setHeader("Cache-Control","min-fresh=2");
482 
483         testDoesNotReturnStaleResponseOnError(req);
484     }
485 
486     @Test
487     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxStale() throws Exception {
488         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
489         req.setHeader("Cache-Control","max-stale=2");
490 
491         testDoesNotReturnStaleResponseOnError(req);
492     }
493 
494     @Test
495     public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxStale() throws Exception {
496         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
497         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
498         req2.setHeader("Cache-Control","max-stale=20");
499 
500         execute(req1);
501 
502         final ClassicHttpResponse result = execute(req2);
503 
504         assertEquals(HttpStatus.SC_OK, result.getCode());
505         assertNotNull(result.getFirstHeader("Warning"));
506 
507         Mockito.verify(mockExecChain, Mockito.atMost(1)).proceed(Mockito.any(), Mockito.any());
508     }
509 
510     /*
511      * "A correct cache MUST respond to a request with the most up-to-date
512      * response held by the cache that is appropriate to the request
513      * (see sections 13.2.5, 13.2.6, and 13.12) which meets one of the
514      * following conditions:
515      *
516      * 1. It has been checked for equivalence with what the origin server
517      * would have returned by revalidating the response with the
518      * origin server (section 13.3);
519      *
520      * 2. It is "fresh enough" (see section 13.2). In the default case,
521      * this means it meets the least restrictive freshness requirement
522      * of the client, origin server, and cache (see section 14.9); if
523      * the origin server so specifies, it is the freshness requirement
524      * of the origin server alone.
525      *
526      * If a stored response is not "fresh enough" by the most
527      * restrictive freshness requirement of both the client and the
528      * origin server, in carefully considered circumstances the cache
529      * MAY still return the response with the appropriate Warning
530      * header (see section 13.1.5 and 14.46), unless such a response
531      * is prohibited (e.g., by a "no-store" cache-directive, or by a
532      * "no-cache" cache-request-directive; see section 14.9).
533      *
534      * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect),
535      * or error (4xx or 5xx) response message.
536      *
537      * If the cache can not communicate with the origin server, then a
538      * correct cache SHOULD respond as above if the response can be
539      * correctly served from the cache..."
540      *
541      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
542      */
543     @Test
544     public void testReturnsCachedResponsesAppropriatelyWhenNoOriginCommunication() throws Exception {
545         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
546         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
547         resp1.setHeader("Cache-Control", "public, max-age=5");
548         resp1.setHeader("ETag","\"etag\"");
549         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
550 
551         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
552 
553         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
554 
555         execute(req1);
556 
557         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
558 
559         final ClassicHttpResponse result = execute(req2);
560 
561         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
562 
563         assertEquals(HttpStatus.SC_OK, result.getCode());
564         boolean warning111Found = false;
565         for(final Header h : result.getHeaders("Warning")) {
566             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
567                 if (wv.getWarnCode() == 111) {
568                     warning111Found = true;
569                     break;
570                 }
571             }
572         }
573         assertTrue(warning111Found);
574     }
575 
576     /*
577      * "If a cache receives a response (either an entire response, or a
578      * 304 (Not Modified) response) that it would normally forward to the
579      * requesting client, and the received response is no longer fresh,
580      * the cache SHOULD forward it to the requesting client without adding
581      * a new Warning (but without removing any existing Warning headers).
582      * A cache SHOULD NOT attempt to revalidate a response simply because
583      * that response became stale in transit; this might lead to an
584      * infinite loop."
585      *
586      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
587      */
588     @Test
589     public void testDoesNotAddNewWarningHeaderIfResponseArrivesStale() throws Exception {
590         originResponse.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
591         originResponse.setHeader("Cache-Control","public, max-age=5");
592         originResponse.setHeader("ETag","\"etag\"");
593 
594         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
595 
596         final ClassicHttpResponse result = execute(request);
597 
598         assertNull(result.getFirstHeader("Warning"));
599     }
600 
601     @Test
602     public void testForwardsExistingWarningHeadersOnResponseThatArrivesStale() throws Exception {
603         originResponse.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
604         originResponse.setHeader("Cache-Control","public, max-age=5");
605         originResponse.setHeader("ETag","\"etag\"");
606         originResponse.addHeader("Age","10");
607         final String warning = "110 fred \"Response is stale\"";
608         originResponse.addHeader("Warning",warning);
609 
610         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
611 
612         final ClassicHttpResponse result = execute(request);
613 
614         assertEquals(warning, result.getFirstHeader("Warning").getValue());
615     }
616 
617     /*
618      * "A transparent proxy SHOULD NOT modify an end-to-end header unless
619      * the definition of that header requires or specifically allows that."
620      *
621      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
622      */
623     private void testDoesNotModifyHeaderOnResponses(final String headerName) throws Exception {
624         final String headerValue = HttpTestUtils
625             .getCanonicalHeaderValue(originResponse, headerName);
626         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
627 
628         final ClassicHttpResponse result = execute(request);
629 
630         assertEquals(headerValue, result.getFirstHeader(headerName).getValue());
631     }
632 
633     private void testDoesNotModifyHeaderOnRequests(final String headerName) throws Exception {
634         final String headerValue = HttpTestUtils.getCanonicalHeaderValue(request, headerName);
635         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
636 
637         execute(request);
638 
639         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
640         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
641 
642         assertEquals(headerValue, HttpTestUtils.getCanonicalHeaderValue(reqCapture.getValue(), headerName));
643     }
644 
645     @Test
646     public void testDoesNotModifyAcceptRangesOnResponses() throws Exception {
647         final String headerName = "Accept-Ranges";
648         originResponse.setHeader(headerName,"bytes");
649         testDoesNotModifyHeaderOnResponses(headerName);
650     }
651 
652     @Test
653     public void testDoesNotModifyAuthorizationOnRequests() throws Exception {
654         request.setHeader("Authorization", StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=");
655         testDoesNotModifyHeaderOnRequests("Authorization");
656     }
657 
658     @Test
659     public void testDoesNotModifyContentLengthOnRequests() throws Exception {
660         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
661         post.setEntity(HttpTestUtils.makeBody(128));
662         post.setHeader("Content-Length","128");
663         request = post;
664         testDoesNotModifyHeaderOnRequests("Content-Length");
665     }
666 
667     @Test
668     public void testDoesNotModifyContentLengthOnResponses() throws Exception {
669         originResponse.setEntity(HttpTestUtils.makeBody(128));
670         originResponse.setHeader("Content-Length","128");
671         testDoesNotModifyHeaderOnResponses("Content-Length");
672     }
673 
674     @Test
675     public void testDoesNotModifyContentMD5OnRequests() throws Exception {
676         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
677         post.setEntity(HttpTestUtils.makeBody(128));
678         post.setHeader("Content-Length","128");
679         post.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
680         request = post;
681         testDoesNotModifyHeaderOnRequests("Content-MD5");
682     }
683 
684     @Test
685     public void testDoesNotModifyContentMD5OnResponses() throws Exception {
686         originResponse.setEntity(HttpTestUtils.makeBody(128));
687         originResponse.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
688         testDoesNotModifyHeaderOnResponses("Content-MD5");
689     }
690 
691     @Test
692     public void testDoesNotModifyContentRangeOnRequests() throws Exception {
693         final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
694         put.setEntity(HttpTestUtils.makeBody(128));
695         put.setHeader("Content-Length","128");
696         put.setHeader("Content-Range","bytes 0-127/256");
697         request = put;
698         testDoesNotModifyHeaderOnRequests("Content-Range");
699     }
700 
701     @Test
702     public void testDoesNotModifyContentRangeOnResponses() throws Exception {
703         request.setHeader("Range","bytes=0-128");
704         originResponse.setCode(HttpStatus.SC_PARTIAL_CONTENT);
705         originResponse.setReasonPhrase("Partial Content");
706         originResponse.setEntity(HttpTestUtils.makeBody(128));
707         originResponse.setHeader("Content-Range","bytes 0-127/256");
708         testDoesNotModifyHeaderOnResponses("Content-Range");
709     }
710 
711     @Test
712     public void testDoesNotModifyContentTypeOnRequests() throws Exception {
713         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
714         post.setEntity(HttpTestUtils.makeBody(128));
715         post.setHeader("Content-Length","128");
716         post.setHeader("Content-Type","application/octet-stream");
717         request = post;
718         testDoesNotModifyHeaderOnRequests("Content-Type");
719     }
720 
721     @Test
722     public void testDoesNotModifyContentTypeOnResponses() throws Exception {
723         originResponse.setHeader("Content-Type","application/octet-stream");
724         testDoesNotModifyHeaderOnResponses("Content-Type");
725     }
726 
727     @Test
728     public void testDoesNotModifyDateOnRequests() throws Exception {
729         request.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
730         testDoesNotModifyHeaderOnRequests("Date");
731     }
732 
733     @Test
734     public void testDoesNotModifyDateOnResponses() throws Exception {
735         originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
736         testDoesNotModifyHeaderOnResponses("Date");
737     }
738 
739     @Test
740     public void testDoesNotModifyETagOnResponses() throws Exception {
741         originResponse.setHeader("ETag", "\"random-etag\"");
742         testDoesNotModifyHeaderOnResponses("ETag");
743     }
744 
745     @Test
746     public void testDoesNotModifyExpiresOnResponses() throws Exception {
747         originResponse.setHeader("Expires", DateUtils.formatStandardDate(Instant.now()));
748         testDoesNotModifyHeaderOnResponses("Expires");
749     }
750 
751     @Test
752     public void testDoesNotModifyFromOnRequests() throws Exception {
753         request.setHeader("From", "foo@example.com");
754         testDoesNotModifyHeaderOnRequests("From");
755     }
756 
757     @Test
758     public void testDoesNotModifyIfMatchOnRequests() throws Exception {
759         request = new BasicClassicHttpRequest("DELETE", "/");
760         request.setHeader("If-Match", "\"etag\"");
761         testDoesNotModifyHeaderOnRequests("If-Match");
762     }
763 
764     @Test
765     public void testDoesNotModifyIfModifiedSinceOnRequests() throws Exception {
766         request.setHeader("If-Modified-Since", DateUtils.formatStandardDate(Instant.now()));
767         testDoesNotModifyHeaderOnRequests("If-Modified-Since");
768     }
769 
770     @Test
771     public void testDoesNotModifyIfNoneMatchOnRequests() throws Exception {
772         request.setHeader("If-None-Match", "\"etag\"");
773         testDoesNotModifyHeaderOnRequests("If-None-Match");
774     }
775 
776     @Test
777     public void testDoesNotModifyIfRangeOnRequests() throws Exception {
778         request.setHeader("Range","bytes=0-128");
779         request.setHeader("If-Range", "\"etag\"");
780         testDoesNotModifyHeaderOnRequests("If-Range");
781     }
782 
783     @Test
784     public void testDoesNotModifyIfUnmodifiedSinceOnRequests() throws Exception {
785         request = new BasicClassicHttpRequest("DELETE", "/");
786         request.setHeader("If-Unmodified-Since", DateUtils.formatStandardDate(Instant.now()));
787         testDoesNotModifyHeaderOnRequests("If-Unmodified-Since");
788     }
789 
790     @Test
791     public void testDoesNotModifyLastModifiedOnResponses() throws Exception {
792         originResponse.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
793         testDoesNotModifyHeaderOnResponses("Last-Modified");
794     }
795 
796     @Test
797     public void testDoesNotModifyLocationOnResponses() throws Exception {
798         originResponse.setCode(HttpStatus.SC_TEMPORARY_REDIRECT);
799         originResponse.setReasonPhrase("Temporary Redirect");
800         originResponse.setHeader("Location", "http://foo.example.com/bar");
801         testDoesNotModifyHeaderOnResponses("Location");
802     }
803 
804     @Test
805     public void testDoesNotModifyRangeOnRequests() throws Exception {
806         request.setHeader("Range", "bytes=0-128");
807         testDoesNotModifyHeaderOnRequests("Range");
808     }
809 
810     @Test
811     public void testDoesNotModifyRefererOnRequests() throws Exception {
812         request.setHeader("Referer", "http://foo.example.com/bar");
813         testDoesNotModifyHeaderOnRequests("Referer");
814     }
815 
816     @Test
817     public void testDoesNotModifyRetryAfterOnResponses() throws Exception {
818         originResponse.setCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
819         originResponse.setReasonPhrase("Service Unavailable");
820         originResponse.setHeader("Retry-After", "120");
821         testDoesNotModifyHeaderOnResponses("Retry-After");
822     }
823 
824     @Test
825     public void testDoesNotModifyServerOnResponses() throws Exception {
826         originResponse.setHeader("Server", "SomeServer/1.0");
827         testDoesNotModifyHeaderOnResponses("Server");
828     }
829 
830     @Test
831     public void testDoesNotModifyUserAgentOnRequests() throws Exception {
832         request.setHeader("User-Agent", "MyClient/1.0");
833         testDoesNotModifyHeaderOnRequests("User-Agent");
834     }
835 
836     @Test
837     public void testDoesNotModifyVaryOnResponses() throws Exception {
838         request.setHeader("Accept-Encoding","identity");
839         originResponse.setHeader("Vary", "Accept-Encoding");
840         testDoesNotModifyHeaderOnResponses("Vary");
841     }
842 
843     @Test
844     public void testDoesNotModifyExtensionHeaderOnRequests() throws Exception {
845         request.setHeader("X-Extension","x-value");
846         testDoesNotModifyHeaderOnRequests("X-Extension");
847     }
848 
849     @Test
850     public void testDoesNotModifyExtensionHeaderOnResponses() throws Exception {
851         originResponse.setHeader("X-Extension", "x-value");
852         testDoesNotModifyHeaderOnResponses("X-Extension");
853     }
854 
855 
856     /*
857      * "[HTTP/1.1 clients], If only a Last-Modified value has been provided
858      * by the origin server, SHOULD use that value in non-subrange cache-
859      * conditional requests (using If-Modified-Since)."
860      *
861      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
862      */
863     @Test
864     public void testUsesLastModifiedDateForCacheConditionalRequests() throws Exception {
865         final Instant twentySecondsAgo = now.plusSeconds(20);
866         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
867 
868         final ClassicHttpRequest req1 =
869             new BasicClassicHttpRequest("GET", "/");
870         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
871         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
872         resp1.setHeader("Last-Modified", lmDate);
873         resp1.setHeader("Cache-Control","max-age=5");
874 
875         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
876         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
877 
878         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
879 
880         execute(req1);
881 
882         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
883 
884         execute(req2);
885 
886         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
887         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
888 
889         final ClassicHttpRequest captured = reqCapture.getValue();
890         final Header ifModifiedSince = captured.getFirstHeader("If-Modified-Since");
891         assertEquals(lmDate, ifModifiedSince.getValue());
892     }
893 
894     /*
895      * "[HTTP/1.1 clients], if both an entity tag and a Last-Modified value
896      * have been provided by the origin server, SHOULD use both validators
897      * in cache-conditional requests. This allows both HTTP/1.0 and
898      * HTTP/1.1 caches to respond appropriately."
899      *
900      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
901      */
902     @Test
903     public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() throws Exception {
904         final Instant twentySecondsAgo = now.plusSeconds(20);
905         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
906         final String etag = "\"etag\"";
907 
908         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
909         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
910         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
911         resp1.setHeader("Last-Modified", lmDate);
912         resp1.setHeader("Cache-Control","max-age=5");
913         resp1.setHeader("ETag", etag);
914 
915         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
916         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
917 
918         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
919 
920         execute(req1);
921 
922         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
923 
924         execute(req2);
925 
926         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
927         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
928 
929         final ClassicHttpRequest captured = reqCapture.getValue();
930         final Header ifModifiedSince = captured.getFirstHeader("If-Modified-Since");
931         assertEquals(lmDate, ifModifiedSince.getValue());
932         final Header ifNoneMatch = captured.getFirstHeader("If-None-Match");
933         assertEquals(etag, ifNoneMatch.getValue());
934     }
935 
936     /*
937      * "If an origin server wishes to force a semantically transparent cache
938      * to validate every request, it MAY assign an explicit expiration time
939      * in the past. This means that the response is always stale, and so the
940      * cache SHOULD validate it before using it for subsequent requests."
941      *
942      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.1
943      */
944     @Test
945     public void testRevalidatesCachedResponseWithExpirationInThePast() throws Exception {
946         final Instant oneSecondAgo = now.minusSeconds(1);
947         final Instant oneSecondFromNow = now.plusSeconds(1);
948         final Instant twoSecondsFromNow = now.plusSeconds(2);
949         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
950         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
951         resp1.setHeader("ETag","\"etag\"");
952         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
953         resp1.setHeader("Expires",DateUtils.formatStandardDate(oneSecondAgo));
954         resp1.setHeader("Cache-Control", "public");
955 
956         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
957 
958         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
959         final ClassicHttpRequest revalidate = new BasicClassicHttpRequest("GET", "/");
960         revalidate.setHeader("If-None-Match","\"etag\"");
961 
962         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
963         resp2.setHeader("Date", DateUtils.formatStandardDate(twoSecondsFromNow));
964         resp2.setHeader("Expires", DateUtils.formatStandardDate(oneSecondFromNow));
965         resp2.setHeader("ETag","\"etag\"");
966 
967         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(revalidate), Mockito.any())).thenReturn(resp2);
968 
969         execute(req1);
970         final ClassicHttpResponse result = execute(req2);
971 
972         assertEquals(HttpStatus.SC_OK, result.getCode());
973     }
974 
975     /* "When a client tries to revalidate a cache entry, and the response
976      * it receives contains a Date header that appears to be older than the
977      * one for the existing entry, then the client SHOULD repeat the
978      * request unconditionally, and include
979      *     Cache-Control: max-age=0
980      * to force any intermediate caches to validate their copies directly
981      * with the origin server, or
982      *     Cache-Control: no-cache
983      * to force any intermediate caches to obtain a new copy from the
984      * origin server."
985      *
986      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.6
987      */
988     @Test
989     public void testRetriesValidationThatResultsInAnOlderDated304Response() throws Exception {
990         final Instant elevenSecondsAgo = now.minusSeconds(11);
991         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
992         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
993         resp1.setHeader("ETag","\"etag\"");
994         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
995         resp1.setHeader("Cache-Control","max-age=5");
996 
997         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
998         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
999         resp2.setHeader("ETag","\"etag\"");
1000         resp2.setHeader("Date", DateUtils.formatStandardDate(elevenSecondsAgo));
1001 
1002         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1003 
1004         execute(req1);
1005 
1006         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1007 
1008         execute(req2);
1009 
1010         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1011         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1012 
1013         final ClassicHttpRequest captured = reqCapture.getValue();
1014         boolean hasMaxAge0 = false;
1015         boolean hasNoCache = false;
1016         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
1017         while (it.hasNext()) {
1018             final HeaderElement elt = it.next();
1019             if ("max-age".equals(elt.getName())) {
1020                 try {
1021                     final int maxage = Integer.parseInt(elt.getValue());
1022                     if (maxage == 0) {
1023                         hasMaxAge0 = true;
1024                     }
1025                 } catch (final NumberFormatException nfe) {
1026                     // nop
1027                 }
1028             } else if ("no-cache".equals(elt.getName())) {
1029                 hasNoCache = true;
1030             }
1031         }
1032         assertTrue(hasMaxAge0 || hasNoCache);
1033         assertNull(captured.getFirstHeader("If-None-Match"));
1034         assertNull(captured.getFirstHeader("If-Modified-Since"));
1035         assertNull(captured.getFirstHeader("If-Range"));
1036         assertNull(captured.getFirstHeader("If-Match"));
1037         assertNull(captured.getFirstHeader("If-Unmodified-Since"));
1038     }
1039 
1040     /* "If an entity tag was assigned to a cached representation, the
1041      * forwarded request SHOULD be conditional and include the entity
1042      * tags in an If-None-Match header field from all its cache entries
1043      * for the resource."
1044      *
1045      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1046      */
1047     @Test
1048     public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
1049         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
1050         req1.setHeader("User-Agent","agent1");
1051         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1052         resp1.setHeader("Cache-Control","max-age=3600");
1053         resp1.setHeader("Vary","User-Agent");
1054         resp1.setHeader("Etag","\"etag1\"");
1055 
1056         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET","/");
1057         req2.setHeader("User-Agent","agent2");
1058         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1059         resp2.setHeader("Cache-Control","max-age=3600");
1060         resp2.setHeader("Vary","User-Agent");
1061         resp2.setHeader("Etag","\"etag2\"");
1062 
1063         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET","/");
1064         req3.setHeader("User-Agent","agent3");
1065         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1066 
1067         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1068 
1069         execute(req1);
1070 
1071         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1072 
1073         execute(req2);
1074 
1075         Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp3);
1076 
1077         execute(req3);
1078 
1079         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1080         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1081 
1082         final ClassicHttpRequest captured = reqCapture.getValue();
1083         boolean foundEtag1 = false;
1084         boolean foundEtag2 = false;
1085         for(final Header h : captured.getHeaders("If-None-Match")) {
1086             for(final String etag : h.getValue().split(",")) {
1087                 if ("\"etag1\"".equals(etag.trim())) {
1088                     foundEtag1 = true;
1089                 }
1090                 if ("\"etag2\"".equals(etag.trim())) {
1091                     foundEtag2 = true;
1092                 }
1093             }
1094         }
1095         assertTrue(foundEtag1 && foundEtag2);
1096     }
1097 
1098     /* "If the entity-tag of the new response matches that of an existing
1099      * entry, the new response SHOULD be used to processChallenge the header fields
1100      * of the existing entry, and the result MUST be returned to the
1101      * client."
1102      *
1103      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1104      */
1105     @Test
1106     public void testResponseToExistingVariantsUpdatesEntry() throws Exception {
1107 
1108         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1109         req1.setHeader("User-Agent", "agent1");
1110 
1111         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1112         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1113         resp1.setHeader("Vary", "User-Agent");
1114         resp1.setHeader("Cache-Control", "max-age=3600");
1115         resp1.setHeader("ETag", "\"etag1\"");
1116 
1117         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1118         req2.setHeader("User-Agent", "agent2");
1119 
1120         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1121         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1122         resp2.setHeader("Vary", "User-Agent");
1123         resp2.setHeader("Cache-Control", "max-age=3600");
1124         resp2.setHeader("ETag", "\"etag2\"");
1125 
1126         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1127         req3.setHeader("User-Agent", "agent3");
1128 
1129         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1130         resp3.setHeader("Date", DateUtils.formatStandardDate(now));
1131         resp3.setHeader("ETag", "\"etag1\"");
1132 
1133         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
1134         req4.setHeader("User-Agent", "agent1");
1135 
1136         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1137 
1138         execute(req1);
1139 
1140         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1141 
1142         execute(req2);
1143 
1144         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1145 
1146         final ClassicHttpResponse result1 = execute(req3);
1147         final ClassicHttpResponse result2 = execute(req4);
1148 
1149         assertEquals(HttpStatus.SC_OK, result1.getCode());
1150         assertEquals("\"etag1\"", result1.getFirstHeader("ETag").getValue());
1151         assertEquals(DateUtils.formatStandardDate(now), result1.getFirstHeader("Date").getValue());
1152         assertEquals(DateUtils.formatStandardDate(now), result2.getFirstHeader("Date").getValue());
1153     }
1154 
1155     @Test
1156     public void testResponseToExistingVariantsIsCachedForFutureResponses() throws Exception {
1157 
1158         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1159         req1.setHeader("User-Agent", "agent1");
1160 
1161         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1162         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1163         resp1.setHeader("Vary", "User-Agent");
1164         resp1.setHeader("Cache-Control", "max-age=3600");
1165         resp1.setHeader("ETag", "\"etag1\"");
1166 
1167         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1168 
1169         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1170         req2.setHeader("User-Agent", "agent2");
1171 
1172         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1173         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1174         resp2.setHeader("ETag", "\"etag1\"");
1175 
1176         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1177 
1178         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1179         req3.setHeader("User-Agent", "agent2");
1180 
1181         execute(req1);
1182         execute(req2);
1183         execute(req3);
1184     }
1185 
1186     /* "If any of the existing cache entries contains only partial content
1187      * for the associated entity, its entity-tag SHOULD NOT be included in
1188      * the If-None-Match header field unless the request is for a range
1189      * that would be fully satisfied by that entry."
1190      *
1191      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1192      */
1193     @Test
1194     public void variantNegotiationsDoNotIncludeEtagsForPartialResponses() throws Exception {
1195         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1196         req1.setHeader("User-Agent", "agent1");
1197         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1198         resp1.setHeader("Cache-Control", "max-age=3600");
1199         resp1.setHeader("Vary", "User-Agent");
1200         resp1.setHeader("ETag", "\"etag1\"");
1201 
1202         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1203         req2.setHeader("User-Agent", "agent2");
1204         req2.setHeader("Range", "bytes=0-49");
1205         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
1206         resp2.setEntity(HttpTestUtils.makeBody(50));
1207         resp2.setHeader("Content-Length","50");
1208         resp2.setHeader("Content-Range","bytes 0-49/100");
1209         resp2.setHeader("Vary","User-Agent");
1210         resp2.setHeader("ETag", "\"etag2\"");
1211         resp2.setHeader("Cache-Control","max-age=3600");
1212         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1213 
1214         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
1215         req3.setHeader("User-Agent", "agent3");
1216 
1217         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1218         resp1.setHeader("Cache-Control", "max-age=3600");
1219         resp1.setHeader("Vary", "User-Agent");
1220         resp1.setHeader("ETag", "\"etag3\"");
1221 
1222         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1223 
1224         execute(req1);
1225 
1226         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1227 
1228         execute(req2);
1229 
1230         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1231 
1232         execute(req3);
1233 
1234         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1235         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1236 
1237         final ClassicHttpRequest captured = reqCapture.getValue();
1238         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.IF_NONE_MATCH);
1239         while (it.hasNext()) {
1240             final HeaderElement elt = it.next();
1241             assertNotEquals("\"etag2\"", elt.toString());
1242         }
1243     }
1244 
1245     /* "If a cache receives a successful response whose Content-Location
1246      * field matches that of an existing cache entry for the same Request-
1247      * URI, whose entity-tag differs from that of the existing entry, and
1248      * whose Date is more recent than that of the existing entry, the
1249      * existing entry SHOULD NOT be returned in response to future requests
1250      * and SHOULD be deleted from the cache.
1251      *
1252      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1253      */
1254     @Test
1255     public void cachedEntryShouldNotBeUsedIfMoreRecentMentionInContentLocation() throws Exception {
1256         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1257         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1258         resp1.setHeader("Cache-Control","max-age=3600");
1259         resp1.setHeader("ETag", "\"old-etag\"");
1260         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1261 
1262         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1263 
1264         final ClassicHttpRequest req2 = new HttpPost("http://foo.example.com/bar");
1265         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1266         resp2.setHeader("ETag", "\"new-etag\"");
1267         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1268         resp2.setHeader("Content-Location", "http://foo.example.com/");
1269 
1270         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1271 
1272         final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
1273         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1274 
1275         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1276 
1277         execute(req1);
1278         execute(req2);
1279         execute(req3);
1280     }
1281 
1282     /*
1283      * "This specifically means that responses from HTTP/1.0 servers for such
1284      * URIs [those containing a '?' in the rel_path part] SHOULD NOT be taken
1285      * from a cache."
1286      *
1287      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
1288      */
1289     @Test
1290     public void responseToGetWithQueryFrom1_0OriginAndNoExpiresIsNotCached() throws Exception {
1291         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
1292         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1293         resp2.setVersion(HttpVersion.HTTP_1_0);
1294         resp2.setEntity(HttpTestUtils.makeBody(200));
1295         resp2.setHeader("Content-Length","200");
1296         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1297 
1298         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1299 
1300         execute(req2);
1301     }
1302 
1303     @Test
1304     public void responseToGetWithQueryFrom1_0OriginVia1_1ProxyAndNoExpiresIsNotCached() throws Exception {
1305         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
1306         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1307         resp2.setVersion(HttpVersion.HTTP_1_0);
1308         resp2.setEntity(HttpTestUtils.makeBody(200));
1309         resp2.setHeader("Content-Length","200");
1310         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1311         resp2.setHeader("Via","1.0 someproxy");
1312 
1313         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1314 
1315         execute(req2);
1316     }
1317 
1318     /*
1319      * "A cache that passes through requests for methods it does not
1320      * understand SHOULD invalidate any entities referred to by the
1321      * Request-URI."
1322      *
1323      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
1324      */
1325     @Test
1326     public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Exception {
1327         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1328         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1329         resp1.setHeader("Cache-Control","max-age=3600");
1330 
1331         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1332 
1333         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("FROB", "/");
1334         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1335         resp2.setHeader("Cache-Control","max-age=3600");
1336 
1337         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1338 
1339         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1340         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1341         resp3.setHeader("ETag", "\"etag\"");
1342 
1343         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1344 
1345         execute(req1);
1346         execute(req2);
1347         final ClassicHttpResponse result = execute(req3);
1348 
1349         assertTrue(HttpTestUtils.semanticallyTransparent(resp3, result));
1350     }
1351 
1352     @Test
1353     public void shouldInvalidateAllVariantsForUnknownMethod() throws Exception {
1354         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1355         req1.setHeader("User-Agent", "agent1");
1356         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1357         resp1.setHeader("Cache-Control","max-age=3600");
1358         resp1.setHeader("Vary", "User-Agent");
1359 
1360         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1361         req2.setHeader("User-Agent", "agent2");
1362         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1363         resp2.setHeader("Cache-Control","max-age=3600");
1364         resp2.setHeader("Vary", "User-Agent");
1365 
1366         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("FROB", "/");
1367         req3.setHeader("User-Agent", "agent3");
1368         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1369         resp3.setHeader("Cache-Control","max-age=3600");
1370 
1371         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
1372         req4.setHeader("User-Agent", "agent1");
1373         final ClassicHttpResponse resp4 = HttpTestUtils.make200Response();
1374         resp4.setHeader("ETag", "\"etag1\"");
1375 
1376         final ClassicHttpRequest req5 = new BasicClassicHttpRequest("GET", "/");
1377         req5.setHeader("User-Agent", "agent2");
1378         final ClassicHttpResponse resp5 = HttpTestUtils.make200Response();
1379         resp5.setHeader("ETag", "\"etag2\"");
1380 
1381         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1382 
1383         execute(req1);
1384 
1385         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1386 
1387         execute(req2);
1388 
1389         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1390 
1391         execute(req3);
1392 
1393         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp4);
1394 
1395         final ClassicHttpResponse result4 = execute(req4);
1396 
1397         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp5);
1398 
1399         final ClassicHttpResponse result5 = execute(req5);
1400 
1401         assertTrue(HttpTestUtils.semanticallyTransparent(resp4, result4));
1402         assertTrue(HttpTestUtils.semanticallyTransparent(resp5, result5));
1403     }
1404 
1405     /*
1406      * "If a new cacheable response is received from a resource while any
1407      * existing responses for the same resource are cached, the cache
1408      * SHOULD use the new response to reply to the current request."
1409      *
1410      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12
1411      */
1412     @Test
1413     public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
1414         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1415         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1416         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1417         resp1.setHeader("Cache-Control", "max-age=3600");
1418         resp1.setHeader("ETag", "\"etag1\"");
1419 
1420         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1421 
1422         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1423         req2.setHeader("Cache-Control", "max-age=0");
1424         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1425         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1426         resp2.setHeader("Cache-Control", "max-age=3600");
1427         resp2.setHeader("ETag", "\"etag2\"");
1428 
1429         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1430 
1431         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
1432 
1433         execute(req1);
1434         execute(req2);
1435         final ClassicHttpResponse result = execute(req3);
1436 
1437         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1438     }
1439 
1440     /*
1441      * "Many HTTP/1.0 cache implementations will treat an Expires value
1442      * that is less than or equal to the response Date value as being
1443      * equivalent to the Cache-Control response directive 'no-cache'.
1444      * If an HTTP/1.1 cache receives such a response, and the response
1445      * does not include a Cache-Control header field, it SHOULD consider
1446      * the response to be non-cacheable in order to retain compatibility
1447      * with HTTP/1.0 servers."
1448      *
1449      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
1450      */
1451     @Test
1452     public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exception {
1453         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1454         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1455         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1456         resp1.setHeader("Expires", DateUtils.formatStandardDate(now));
1457         resp1.removeHeaders("Cache-Control");
1458 
1459         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1460 
1461         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1462         req2.setHeader("Cache-Control", "max-stale=1000");
1463         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1464         resp2.setHeader("ETag", "\"etag2\"");
1465 
1466         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1467 
1468         execute(req1);
1469         final ClassicHttpResponse result = execute(req2);
1470 
1471         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1472     }
1473 
1474     @Test
1475     public void expiresPriorToDateWithNoCacheControlIsNotCacheable() throws Exception {
1476         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1477         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1478         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1479         resp1.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
1480         resp1.removeHeaders("Cache-Control");
1481 
1482         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1483 
1484         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1485         req2.setHeader("Cache-Control", "max-stale=1000");
1486         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1487         resp2.setHeader("ETag", "\"etag2\"");
1488 
1489         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1490 
1491         execute(req1);
1492         final ClassicHttpResponse result = execute(req2);
1493 
1494         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1495     }
1496 
1497     /*
1498      * "If a request includes the no-cache directive, it SHOULD NOT
1499      * include min-fresh, max-stale, or max-age."
1500      *
1501      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
1502      */
1503     @Test
1504     public void otherFreshnessRequestDirectivesNotAllowedWithNoCache() throws Exception {
1505         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1506         req1.setHeader("Cache-Control", "min-fresh=10, no-cache");
1507         req1.addHeader("Cache-Control", "max-stale=0, max-age=0");
1508 
1509         execute(req1);
1510 
1511         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1512         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
1513 
1514         final ClassicHttpRequest captured = reqCapture.getValue();
1515         boolean foundNoCache = false;
1516         boolean foundDisallowedDirective = false;
1517         final List<String> disallowed =
1518             Arrays.asList("min-fresh", "max-stale", "max-age");
1519         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
1520         while (it.hasNext()) {
1521             final HeaderElement elt = it.next();
1522             if (disallowed.contains(elt.getName())) {
1523                 foundDisallowedDirective = true;
1524             }
1525             if ("no-cache".equals(elt.getName())) {
1526                 foundNoCache = true;
1527             }
1528         }
1529         assertTrue(foundNoCache);
1530         assertFalse(foundDisallowedDirective);
1531     }
1532 
1533     /*
1534      * "To do this, the client may include the only-if-cached directive in
1535      * a request. If it receives this directive, a cache SHOULD either
1536      * respond using a cached entry that is consistent with the other
1537      * constraints of the request, or respond with a 504 (Gateway Timeout)
1538      * status."
1539      *
1540      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
1541      */
1542     @Test
1543     public void cacheMissResultsIn504WithOnlyIfCached() throws Exception {
1544         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
1545         req.setHeader("Cache-Control", "only-if-cached");
1546 
1547         final ClassicHttpResponse result = execute(req);
1548 
1549         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1550     }
1551 
1552     @Test
1553     public void cacheHitOkWithOnlyIfCached() throws Exception {
1554         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1555         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1556         resp1.setHeader("Cache-Control","max-age=3600");
1557 
1558         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1559 
1560         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1561         req2.setHeader("Cache-Control", "only-if-cached");
1562 
1563         execute(req1);
1564         final ClassicHttpResponse result = execute(req2);
1565 
1566         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1567     }
1568 
1569     @Test
1570     public void returns504ForStaleEntryWithOnlyIfCached() throws Exception {
1571         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1572         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1573         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1574         resp1.setHeader("Cache-Control","max-age=5");
1575 
1576         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1577 
1578         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1579         req2.setHeader("Cache-Control", "only-if-cached");
1580 
1581         execute(req1);
1582         final ClassicHttpResponse result = execute(req2);
1583 
1584         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1585     }
1586 
1587     @Test
1588     public void returnsStaleCacheEntryWithOnlyIfCachedAndMaxStale() throws Exception {
1589 
1590         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1591         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1592         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1593         resp1.setHeader("Cache-Control","max-age=5");
1594 
1595         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1596 
1597         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1598         req2.setHeader("Cache-Control", "max-stale=20, only-if-cached");
1599 
1600         execute(req1);
1601         final ClassicHttpResponse result = execute(req2);
1602 
1603         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1604     }
1605 
1606     @Test
1607     public void issues304EvenWithWeakETag() throws Exception {
1608         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1609         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1610         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1611         resp1.setHeader("Cache-Control", "max-age=300");
1612         resp1.setHeader("ETag","W/\"weak-sauce\"");
1613 
1614         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1615 
1616         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1617         req2.setHeader("If-None-Match","W/\"weak-sauce\"");
1618 
1619         execute(req1);
1620         final ClassicHttpResponse result = execute(req2);
1621 
1622         assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
1623     }
1624 
1625 }