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  import static org.junit.jupiter.api.Assertions.assertEquals;
30  import static org.junit.jupiter.api.Assertions.assertFalse;
31  import static org.junit.jupiter.api.Assertions.assertNotEquals;
32  import static org.junit.jupiter.api.Assertions.assertTrue;
33  
34  import java.io.IOException;
35  import java.time.Instant;
36  import java.time.temporal.ChronoUnit;
37  import java.util.Iterator;
38  
39  import org.apache.hc.client5.http.HttpRoute;
40  import org.apache.hc.client5.http.auth.StandardAuthScheme;
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.protocol.HttpClientContext;
44  import org.apache.hc.client5.http.utils.DateUtils;
45  import org.apache.hc.core5.http.ClassicHttpRequest;
46  import org.apache.hc.core5.http.ClassicHttpResponse;
47  import org.apache.hc.core5.http.Header;
48  import org.apache.hc.core5.http.HeaderElement;
49  import org.apache.hc.core5.http.HttpEntity;
50  import org.apache.hc.core5.http.HttpException;
51  import org.apache.hc.core5.http.HttpHeaders;
52  import org.apache.hc.core5.http.HttpHost;
53  import org.apache.hc.core5.http.HttpStatus;
54  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
55  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
56  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
57  import org.apache.hc.core5.http.message.MessageSupport;
58  import org.hamcrest.MatcherAssert;
59  import org.junit.jupiter.api.BeforeEach;
60  import org.junit.jupiter.api.Test;
61  import org.mockito.ArgumentCaptor;
62  import org.mockito.Mock;
63  import org.mockito.Mockito;
64  import org.mockito.MockitoAnnotations;
65  
66  /*
67   * This test class captures functionality required to achieve unconditional
68   * compliance with the HTTP/1.1 caching protocol (SHOULD, SHOULD NOT,
69   * RECOMMENDED, and NOT RECOMMENDED behaviors).
70   */
71  public class TestProtocolRecommendations {
72  
73      static final int MAX_BYTES = 1024;
74      static final int MAX_ENTRIES = 100;
75      static final int ENTITY_LENGTH = 128;
76  
77      HttpHost host;
78      HttpRoute route;
79      HttpEntity body;
80      HttpClientContext context;
81      @Mock
82      ExecChain mockExecChain;
83      @Mock
84      ExecRuntime mockExecRuntime;
85      ClassicHttpRequest request;
86      ClassicHttpResponse originResponse;
87      CacheConfig config;
88      CachingExec impl;
89      HttpCache cache;
90      Instant now;
91      Instant tenSecondsAgo;
92      Instant twoMinutesAgo;
93  
94      @BeforeEach
95      public void setUp() throws Exception {
96          MockitoAnnotations.openMocks(this);
97          host = new HttpHost("foo.example.com", 80);
98  
99          route = new HttpRoute(host);
100 
101         body = HttpTestUtils.makeBody(ENTITY_LENGTH);
102 
103         request = new BasicClassicHttpRequest("GET", "/foo");
104 
105         context = HttpClientContext.create();
106 
107         originResponse = HttpTestUtils.make200Response();
108 
109         config = CacheConfig.custom()
110                 .setMaxCacheEntries(MAX_ENTRIES)
111                 .setMaxObjectSize(MAX_BYTES)
112                 .build();
113 
114         cache = new BasicHttpCache(config);
115         impl = new CachingExec(cache, null, config);
116 
117         now = Instant.now();
118         tenSecondsAgo = now.minus(10, ChronoUnit.SECONDS);
119         twoMinutesAgo = now.minus(1, ChronoUnit.MINUTES);
120 
121         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
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     private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
132             final String headerName, final String headerValue, final String validatorHeader,
133             final String validator, final String conditionalHeader) throws Exception {
134         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
135         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
136         resp1.setHeader("Cache-Control","max-age=3600");
137         resp1.setHeader(validatorHeader, validator);
138         resp1.setHeader(headerName, headerValue);
139         resp1.setHeader("ETag", "\"etag\"");
140 
141         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
142 
143         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
144         req2.setHeader(conditionalHeader, validator);
145 
146         execute(req1);
147         final ClassicHttpResponse result = execute(req2);
148 
149         assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
150         assertFalse(result.containsHeader(headerName));
151     }
152 
153     private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
154             final String headerName, final String headerValue) throws Exception {
155         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
156                 headerValue, "ETag", "\"etag\"", "If-None-Match");
157     }
158 
159     private void cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
160             final String headerName, final String headerValue) throws Exception {
161         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
162                 headerValue, "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo),
163                 "If-Modified-Since");
164     }
165 
166     @Test
167     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainAllow() throws Exception {
168         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
169                 "Allow", "GET,HEAD");
170     }
171 
172     @Test
173     public void cacheGenerated304ForStrongDateValidatorShouldNotContainAllow() throws Exception {
174         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
175                 "Allow", "GET,HEAD");
176     }
177 
178     @Test
179     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentEncoding() throws Exception {
180         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
181                 "Content-Encoding", "gzip");
182     }
183 
184     @Test
185     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentEncoding() throws Exception {
186         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
187                 "Content-Encoding", "gzip");
188     }
189 
190     @Test
191     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentLanguage() throws Exception {
192         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
193                 "Content-Language", "en");
194     }
195 
196     @Test
197     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLanguage() throws Exception {
198         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
199                 "Content-Language", "en");
200     }
201 
202     @Test
203     public void cacheGenerated304ForStrongValidatorShouldNotContainContentLength() throws Exception {
204         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
205                 "Content-Length", "128");
206     }
207 
208     @Test
209     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLength() throws Exception {
210         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
211                 "Content-Length", "128");
212     }
213 
214     @Test
215     public void cacheGenerated304ForStrongValidatorShouldNotContainContentMD5() throws Exception {
216         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
217                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
218     }
219 
220     @Test
221     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5() throws Exception {
222         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
223                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
224     }
225 
226     @Test
227     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType() throws Exception {
228         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
229                 "Content-Type", "text/html");
230     }
231 
232     @Test
233     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentType() throws Exception {
234         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
235                 "Content-Type", "text/html");
236     }
237 
238     @Test
239     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainLastModified() throws Exception {
240         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
241                 "Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
242     }
243 
244     @Test
245     public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified() throws Exception {
246         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
247                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
248     }
249 
250     private ClassicHttpRequest requestToPopulateStaleCacheEntry() throws Exception {
251         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
252         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
253         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
254         resp1.setHeader("Cache-Control","public,max-age=5");
255         resp1.setHeader("Etag","\"etag\"");
256 
257         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
258         return req1;
259     }
260 
261     private void testDoesNotReturnStaleResponseOnError(final ClassicHttpRequest req2) throws Exception {
262         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
263 
264         execute(req1);
265 
266         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
267 
268         ClassicHttpResponse result = null;
269         try {
270             result = execute(req2);
271         } catch (final IOException acceptable) {
272         }
273 
274         if (result != null) {
275             assertNotEquals(HttpStatus.SC_OK, result.getCode());
276         }
277     }
278 
279     @Test
280     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithCacheControl() throws Exception {
281         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
282         req.setHeader("Cache-Control","no-cache");
283         testDoesNotReturnStaleResponseOnError(req);
284     }
285 
286     @Test
287     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxAge() throws Exception {
288         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
289         req.setHeader("Cache-Control","max-age=0");
290         testDoesNotReturnStaleResponseOnError(req);
291     }
292 
293     @Test
294     public void testDoesNotReturnStaleResponseIfClientExplicitlySpecifiesLargerMaxAge() throws Exception {
295         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
296         req.setHeader("Cache-Control","max-age=20");
297         testDoesNotReturnStaleResponseOnError(req);
298     }
299 
300 
301     @Test
302     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMinFresh() throws Exception {
303         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
304         req.setHeader("Cache-Control","min-fresh=2");
305 
306         testDoesNotReturnStaleResponseOnError(req);
307     }
308 
309     @Test
310     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxStale() throws Exception {
311         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
312         req.setHeader("Cache-Control","max-stale=2");
313 
314         testDoesNotReturnStaleResponseOnError(req);
315     }
316 
317     @Test
318     public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxStale() throws Exception {
319         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
320         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
321         req2.setHeader("Cache-Control","max-stale=20");
322 
323         execute(req1);
324 
325         final ClassicHttpResponse result = execute(req2);
326 
327         assertEquals(HttpStatus.SC_OK, result.getCode());
328 
329         Mockito.verify(mockExecChain, Mockito.atMost(1)).proceed(Mockito.any(), Mockito.any());
330     }
331 
332     private void testDoesNotModifyHeaderOnResponses(final String headerName) throws Exception {
333         final String headerValue = HttpTestUtils
334             .getCanonicalHeaderValue(originResponse, headerName);
335         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
336 
337         final ClassicHttpResponse result = execute(request);
338 
339         MatcherAssert.assertThat(result, ContainsHeaderMatcher.contains(headerName, headerValue));
340     }
341 
342     private void testDoesNotModifyHeaderOnRequests(final String headerName) throws Exception {
343         final String headerValue = HttpTestUtils.getCanonicalHeaderValue(request, headerName);
344         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
345 
346         execute(request);
347 
348         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
349         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
350 
351         assertEquals(headerValue, HttpTestUtils.getCanonicalHeaderValue(reqCapture.getValue(), headerName));
352     }
353 
354     @Test
355     public void testDoesNotModifyAcceptRangesOnResponses() throws Exception {
356         final String headerName = "Accept-Ranges";
357         originResponse.setHeader(headerName,"bytes");
358         testDoesNotModifyHeaderOnResponses(headerName);
359     }
360 
361     @Test
362     public void testDoesNotModifyAuthorizationOnRequests() throws Exception {
363         request.setHeader("Authorization", StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=");
364         testDoesNotModifyHeaderOnRequests("Authorization");
365     }
366 
367     @Test
368     public void testDoesNotModifyContentLengthOnRequests() throws Exception {
369         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
370         post.setEntity(HttpTestUtils.makeBody(128));
371         post.setHeader("Content-Length","128");
372         request = post;
373         testDoesNotModifyHeaderOnRequests("Content-Length");
374     }
375 
376     @Test
377     public void testDoesNotModifyContentLengthOnResponses() throws Exception {
378         originResponse.setEntity(HttpTestUtils.makeBody(128));
379         originResponse.setHeader("Content-Length","128");
380         testDoesNotModifyHeaderOnResponses("Content-Length");
381     }
382 
383     @Test
384     public void testDoesNotModifyContentMD5OnRequests() throws Exception {
385         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
386         post.setEntity(HttpTestUtils.makeBody(128));
387         post.setHeader("Content-Length","128");
388         post.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
389         request = post;
390         testDoesNotModifyHeaderOnRequests("Content-MD5");
391     }
392 
393     @Test
394     public void testDoesNotModifyContentMD5OnResponses() throws Exception {
395         originResponse.setEntity(HttpTestUtils.makeBody(128));
396         originResponse.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
397         testDoesNotModifyHeaderOnResponses("Content-MD5");
398     }
399 
400     @Test
401     public void testDoesNotModifyContentRangeOnRequests() throws Exception {
402         final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
403         put.setEntity(HttpTestUtils.makeBody(128));
404         put.setHeader("Content-Length","128");
405         put.setHeader("Content-Range","bytes 0-127/256");
406         request = put;
407         testDoesNotModifyHeaderOnRequests("Content-Range");
408     }
409 
410     @Test
411     public void testDoesNotModifyContentRangeOnResponses() throws Exception {
412         request.setHeader("Range","bytes=0-128");
413         originResponse.setCode(HttpStatus.SC_PARTIAL_CONTENT);
414         originResponse.setReasonPhrase("Partial Content");
415         originResponse.setEntity(HttpTestUtils.makeBody(128));
416         originResponse.setHeader("Content-Range","bytes 0-127/256");
417         testDoesNotModifyHeaderOnResponses("Content-Range");
418     }
419 
420     @Test
421     public void testDoesNotModifyContentTypeOnRequests() throws Exception {
422         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
423         post.setEntity(HttpTestUtils.makeBody(128));
424         post.setHeader("Content-Length","128");
425         post.setHeader("Content-Type","application/octet-stream");
426         request = post;
427         testDoesNotModifyHeaderOnRequests("Content-Type");
428     }
429 
430     @Test
431     public void testDoesNotModifyContentTypeOnResponses() throws Exception {
432         originResponse.setHeader("Content-Type","application/octet-stream");
433         testDoesNotModifyHeaderOnResponses("Content-Type");
434     }
435 
436     @Test
437     public void testDoesNotModifyDateOnRequests() throws Exception {
438         request.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
439         testDoesNotModifyHeaderOnRequests("Date");
440     }
441 
442     @Test
443     public void testDoesNotModifyDateOnResponses() throws Exception {
444         originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
445         testDoesNotModifyHeaderOnResponses("Date");
446     }
447 
448     @Test
449     public void testDoesNotModifyETagOnResponses() throws Exception {
450         originResponse.setHeader("ETag", "\"random-etag\"");
451         testDoesNotModifyHeaderOnResponses("ETag");
452     }
453 
454     @Test
455     public void testDoesNotModifyExpiresOnResponses() throws Exception {
456         originResponse.setHeader("Expires", DateUtils.formatStandardDate(Instant.now()));
457         testDoesNotModifyHeaderOnResponses("Expires");
458     }
459 
460     @Test
461     public void testDoesNotModifyFromOnRequests() throws Exception {
462         request.setHeader("From", "foo@example.com");
463         testDoesNotModifyHeaderOnRequests("From");
464     }
465 
466     @Test
467     public void testDoesNotModifyIfMatchOnRequests() throws Exception {
468         request = new BasicClassicHttpRequest("DELETE", "/");
469         request.setHeader("If-Match", "\"etag\"");
470         testDoesNotModifyHeaderOnRequests("If-Match");
471     }
472 
473     @Test
474     public void testDoesNotModifyIfModifiedSinceOnRequests() throws Exception {
475         request.setHeader("If-Modified-Since", DateUtils.formatStandardDate(Instant.now()));
476         testDoesNotModifyHeaderOnRequests("If-Modified-Since");
477     }
478 
479     @Test
480     public void testDoesNotModifyIfNoneMatchOnRequests() throws Exception {
481         request.setHeader("If-None-Match", "\"etag\"");
482         testDoesNotModifyHeaderOnRequests("If-None-Match");
483     }
484 
485     @Test
486     public void testDoesNotModifyIfRangeOnRequests() throws Exception {
487         request.setHeader("Range","bytes=0-128");
488         request.setHeader("If-Range", "\"etag\"");
489         testDoesNotModifyHeaderOnRequests("If-Range");
490     }
491 
492     @Test
493     public void testDoesNotModifyIfUnmodifiedSinceOnRequests() throws Exception {
494         request = new BasicClassicHttpRequest("DELETE", "/");
495         request.setHeader("If-Unmodified-Since", DateUtils.formatStandardDate(Instant.now()));
496         testDoesNotModifyHeaderOnRequests("If-Unmodified-Since");
497     }
498 
499     @Test
500     public void testDoesNotModifyLastModifiedOnResponses() throws Exception {
501         originResponse.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
502         testDoesNotModifyHeaderOnResponses("Last-Modified");
503     }
504 
505     @Test
506     public void testDoesNotModifyLocationOnResponses() throws Exception {
507         originResponse.setCode(HttpStatus.SC_TEMPORARY_REDIRECT);
508         originResponse.setReasonPhrase("Temporary Redirect");
509         originResponse.setHeader("Location", "http://foo.example.com/bar");
510         testDoesNotModifyHeaderOnResponses("Location");
511     }
512 
513     @Test
514     public void testDoesNotModifyRangeOnRequests() throws Exception {
515         request.setHeader("Range", "bytes=0-128");
516         testDoesNotModifyHeaderOnRequests("Range");
517     }
518 
519     @Test
520     public void testDoesNotModifyRefererOnRequests() throws Exception {
521         request.setHeader("Referer", "http://foo.example.com/bar");
522         testDoesNotModifyHeaderOnRequests("Referer");
523     }
524 
525     @Test
526     public void testDoesNotModifyRetryAfterOnResponses() throws Exception {
527         originResponse.setCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
528         originResponse.setReasonPhrase("Service Unavailable");
529         originResponse.setHeader("Retry-After", "120");
530         testDoesNotModifyHeaderOnResponses("Retry-After");
531     }
532 
533     @Test
534     public void testDoesNotModifyServerOnResponses() throws Exception {
535         originResponse.setHeader("Server", "SomeServer/1.0");
536         testDoesNotModifyHeaderOnResponses("Server");
537     }
538 
539     @Test
540     public void testDoesNotModifyUserAgentOnRequests() throws Exception {
541         request.setHeader("User-Agent", "MyClient/1.0");
542         testDoesNotModifyHeaderOnRequests("User-Agent");
543     }
544 
545     @Test
546     public void testDoesNotModifyVaryOnResponses() throws Exception {
547         request.setHeader("Accept-Encoding","identity");
548         originResponse.setHeader("Vary", "Accept-Encoding");
549         testDoesNotModifyHeaderOnResponses("Vary");
550     }
551 
552     @Test
553     public void testDoesNotModifyExtensionHeaderOnRequests() throws Exception {
554         request.setHeader("X-Extension","x-value");
555         testDoesNotModifyHeaderOnRequests("X-Extension");
556     }
557 
558     @Test
559     public void testDoesNotModifyExtensionHeaderOnResponses() throws Exception {
560         originResponse.setHeader("X-Extension", "x-value");
561         testDoesNotModifyHeaderOnResponses("X-Extension");
562     }
563 
564     @Test
565     public void testUsesLastModifiedDateForCacheConditionalRequests() throws Exception {
566         final Instant twentySecondsAgo = now.plusSeconds(20);
567         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
568 
569         final ClassicHttpRequest req1 =
570             new BasicClassicHttpRequest("GET", "/");
571         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
572         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
573         resp1.setHeader("Last-Modified", lmDate);
574         resp1.setHeader("Cache-Control","max-age=5");
575 
576         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
577         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
578 
579         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
580 
581         execute(req1);
582 
583         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
584 
585         execute(req2);
586 
587         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
588         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
589 
590         final ClassicHttpRequest captured = reqCapture.getValue();
591         MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-Modified-Since", lmDate));
592     }
593 
594     @Test
595     public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() throws Exception {
596         final Instant twentySecondsAgo = now.plusSeconds(20);
597         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
598         final String etag = "\"etag\"";
599 
600         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
601         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
602         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
603         resp1.setHeader("Last-Modified", lmDate);
604         resp1.setHeader("Cache-Control","max-age=5");
605         resp1.setHeader("ETag", etag);
606 
607         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
608         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
609 
610         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
611 
612         execute(req1);
613 
614         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
615 
616         execute(req2);
617 
618         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
619         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
620 
621         final ClassicHttpRequest captured = reqCapture.getValue();
622 
623         MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-Modified-Since", lmDate));
624         MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-None-Match", etag));
625     }
626 
627     @Test
628     public void testRevalidatesCachedResponseWithExpirationInThePast() throws Exception {
629         final Instant oneSecondAgo = now.minusSeconds(1);
630         final Instant oneSecondFromNow = now.plusSeconds(1);
631         final Instant twoSecondsFromNow = now.plusSeconds(2);
632         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
633         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
634         resp1.setHeader("ETag","\"etag\"");
635         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
636         resp1.setHeader("Expires",DateUtils.formatStandardDate(oneSecondAgo));
637         resp1.setHeader("Cache-Control", "public");
638 
639         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
640 
641         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
642         final ClassicHttpRequest revalidate = new BasicClassicHttpRequest("GET", "/");
643         revalidate.setHeader("If-None-Match","\"etag\"");
644 
645         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
646         resp2.setHeader("Date", DateUtils.formatStandardDate(twoSecondsFromNow));
647         resp2.setHeader("Expires", DateUtils.formatStandardDate(oneSecondFromNow));
648         resp2.setHeader("ETag","\"etag\"");
649 
650         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(revalidate), Mockito.any())).thenReturn(resp2);
651 
652         execute(req1);
653         final ClassicHttpResponse result = execute(req2);
654 
655         assertEquals(HttpStatus.SC_OK, result.getCode());
656     }
657 
658     @Test
659     public void testRetriesValidationThatResultsInAnOlderDated304Response() throws Exception {
660         final Instant elevenSecondsAgo = now.minusSeconds(11);
661         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
662         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
663         resp1.setHeader("ETag","\"etag\"");
664         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
665         resp1.setHeader("Cache-Control","max-age=5");
666 
667         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
668         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
669         resp2.setHeader("ETag","\"etag\"");
670         resp2.setHeader("Date", DateUtils.formatStandardDate(elevenSecondsAgo));
671 
672         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
673 
674         execute(req1);
675 
676         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
677 
678         execute(req2);
679 
680         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
681         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
682 
683         final ClassicHttpRequest captured = reqCapture.getValue();
684         boolean hasMaxAge0 = false;
685         boolean hasNoCache = false;
686         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
687         while (it.hasNext()) {
688             final HeaderElement elt = it.next();
689             if ("max-age".equals(elt.getName())) {
690                 try {
691                     final int maxage = Integer.parseInt(elt.getValue());
692                     if (maxage == 0) {
693                         hasMaxAge0 = true;
694                     }
695                 } catch (final NumberFormatException nfe) {
696                     // nop
697                 }
698             } else if ("no-cache".equals(elt.getName())) {
699                 hasNoCache = true;
700             }
701         }
702         assertTrue(hasMaxAge0 || hasNoCache);
703         assertFalse(captured.containsHeader("If-None-Match"));
704         assertFalse(captured.containsHeader("If-Modified-Since"));
705         assertFalse(captured.containsHeader("If-Range"));
706         assertFalse(captured.containsHeader("If-Match"));
707         assertFalse(captured.containsHeader("If-Unmodified-Since"));
708     }
709 
710     @Test
711     public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
712         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
713         req1.setHeader("User-Agent","agent1");
714         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
715         resp1.setHeader("Cache-Control","max-age=3600");
716         resp1.setHeader("Vary","User-Agent");
717         resp1.setHeader("Etag","\"etag1\"");
718 
719         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET","/");
720         req2.setHeader("User-Agent","agent2");
721         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
722         resp2.setHeader("Cache-Control","max-age=3600");
723         resp2.setHeader("Vary","User-Agent");
724         resp2.setHeader("Etag","\"etag2\"");
725 
726         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET","/");
727         req3.setHeader("User-Agent","agent3");
728         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
729 
730         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
731 
732         execute(req1);
733 
734         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
735 
736         execute(req2);
737 
738         Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp3);
739 
740         execute(req3);
741 
742         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
743         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
744 
745         final ClassicHttpRequest captured = reqCapture.getValue();
746         boolean foundEtag1 = false;
747         boolean foundEtag2 = false;
748         for(final Header h : captured.getHeaders("If-None-Match")) {
749             for(final String etag : h.getValue().split(",")) {
750                 if ("\"etag1\"".equals(etag.trim())) {
751                     foundEtag1 = true;
752                 }
753                 if ("\"etag2\"".equals(etag.trim())) {
754                     foundEtag2 = true;
755                 }
756             }
757         }
758         assertTrue(foundEtag1 && foundEtag2);
759     }
760 
761     @Test
762     public void testResponseToExistingVariantsUpdatesEntry() throws Exception {
763 
764         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
765         req1.setHeader("User-Agent", "agent1");
766 
767         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
768         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
769         resp1.setHeader("Vary", "User-Agent");
770         resp1.setHeader("Cache-Control", "max-age=3600");
771         resp1.setHeader("ETag", "\"etag1\"");
772 
773         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
774         req2.setHeader("User-Agent", "agent2");
775 
776         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
777         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
778         resp2.setHeader("Vary", "User-Agent");
779         resp2.setHeader("Cache-Control", "max-age=3600");
780         resp2.setHeader("ETag", "\"etag2\"");
781 
782         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
783         req3.setHeader("User-Agent", "agent3");
784 
785         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
786         resp3.setHeader("Date", DateUtils.formatStandardDate(now));
787         resp3.setHeader("ETag", "\"etag1\"");
788 
789         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
790         req4.setHeader("User-Agent", "agent1");
791 
792         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
793         execute(req1);
794 
795         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
796         execute(req2);
797 
798         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
799         final ClassicHttpResponse result1 = execute(req3);
800 
801         final ClassicHttpResponse result2 = execute(req4);
802 
803         assertEquals(HttpStatus.SC_OK, result1.getCode());
804         MatcherAssert.assertThat(result1, ContainsHeaderMatcher.contains("ETag", "\"etag1\""));
805         MatcherAssert.assertThat(result1, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(now)));
806         MatcherAssert.assertThat(result2, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(now)));
807     }
808 
809     @Test
810     public void testResponseToExistingVariantsIsCachedForFutureResponses() throws Exception {
811 
812         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
813         req1.setHeader("User-Agent", "agent1");
814 
815         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
816         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
817         resp1.setHeader("Vary", "User-Agent");
818         resp1.setHeader("Cache-Control", "max-age=3600");
819         resp1.setHeader("ETag", "\"etag1\"");
820 
821         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
822 
823         execute(req1);
824 
825         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
826         req2.setHeader("User-Agent", "agent2");
827 
828         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
829         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
830         resp2.setHeader("ETag", "\"etag1\"");
831 
832         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
833 
834         execute(req2);
835 
836         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
837         req3.setHeader("User-Agent", "agent2");
838 
839         execute(req3);
840 
841         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
842     }
843 
844     @Test
845     public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Exception {
846         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
847         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
848         resp1.setHeader("Cache-Control","max-age=3600");
849 
850         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
851 
852         execute(req1);
853 
854         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("FROB", "/");
855         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
856         resp2.setHeader("Cache-Control","max-age=3600");
857 
858         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
859 
860         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
861         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
862         resp3.setHeader("ETag", "\"etag\"");
863 
864         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
865 
866         execute(req2);
867         final ClassicHttpResponse result = execute(req3);
868 
869         assertTrue(HttpTestUtils.semanticallyTransparent(resp3, result));
870     }
871 
872     @Test
873     public void shouldInvalidateAllVariantsForUnknownMethod() throws Exception {
874         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
875         req1.setHeader("User-Agent", "agent1");
876         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
877         resp1.setHeader("Cache-Control","max-age=3600");
878         resp1.setHeader("Vary", "User-Agent");
879 
880         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
881         req2.setHeader("User-Agent", "agent2");
882         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
883         resp2.setHeader("Cache-Control","max-age=3600");
884         resp2.setHeader("Vary", "User-Agent");
885 
886         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("FROB", "/");
887         req3.setHeader("User-Agent", "agent3");
888         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
889         resp3.setHeader("Cache-Control","max-age=3600");
890 
891         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
892         req4.setHeader("User-Agent", "agent1");
893         final ClassicHttpResponse resp4 = HttpTestUtils.make200Response();
894         resp4.setHeader("ETag", "\"etag1\"");
895 
896         final ClassicHttpRequest req5 = new BasicClassicHttpRequest("GET", "/");
897         req5.setHeader("User-Agent", "agent2");
898         final ClassicHttpResponse resp5 = HttpTestUtils.make200Response();
899         resp5.setHeader("ETag", "\"etag2\"");
900 
901         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
902 
903         execute(req1);
904 
905         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
906 
907         execute(req2);
908 
909         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
910 
911         execute(req3);
912 
913         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp4);
914 
915         final ClassicHttpResponse result4 = execute(req4);
916 
917         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp5);
918 
919         final ClassicHttpResponse result5 = execute(req5);
920 
921         assertTrue(HttpTestUtils.semanticallyTransparent(resp4, result4));
922         assertTrue(HttpTestUtils.semanticallyTransparent(resp5, result5));
923     }
924 
925     @Test
926     public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
927         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
928         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
929         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
930         resp1.setHeader("Cache-Control", "max-age=3600");
931         resp1.setHeader("ETag", "\"etag1\"");
932 
933         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
934 
935         execute(req1);
936 
937         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
938         req2.setHeader("Cache-Control", "max-age=0");
939         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
940         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
941         resp2.setHeader("Cache-Control", "max-age=3600");
942         resp2.setHeader("ETag", "\"etag2\"");
943 
944         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
945 
946         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
947 
948         execute(req2);
949         final ClassicHttpResponse result = execute(req3);
950 
951         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
952     }
953 
954     @Test
955     public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exception {
956         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
957         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
958         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
959         resp1.setHeader("Expires", DateUtils.formatStandardDate(now));
960         resp1.removeHeaders("Cache-Control");
961 
962         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
963 
964         execute(req1);
965 
966         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
967         req2.setHeader("Cache-Control", "max-stale=1000");
968         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
969         resp2.setHeader("ETag", "\"etag2\"");
970 
971         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
972 
973         final ClassicHttpResponse result = execute(req2);
974 
975         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
976     }
977 
978     @Test
979     public void expiresPriorToDateWithNoCacheControlIsNotCacheable() throws Exception {
980         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
981         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
982         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
983         resp1.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
984         resp1.removeHeaders("Cache-Control");
985 
986         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
987 
988         execute(req1);
989 
990         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
991         req2.setHeader("Cache-Control", "max-stale=1000");
992         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
993         resp2.setHeader("ETag", "\"etag2\"");
994 
995         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
996 
997         final ClassicHttpResponse result = execute(req2);
998 
999         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1000     }
1001 
1002     @Test
1003     public void cacheMissResultsIn504WithOnlyIfCached() throws Exception {
1004         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
1005         req.setHeader("Cache-Control", "only-if-cached");
1006 
1007         final ClassicHttpResponse result = execute(req);
1008 
1009         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1010     }
1011 
1012     @Test
1013     public void cacheHitOkWithOnlyIfCached() throws Exception {
1014         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1015         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1016         resp1.setHeader("Cache-Control","max-age=3600");
1017 
1018         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1019 
1020         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1021         req2.setHeader("Cache-Control", "only-if-cached");
1022 
1023         execute(req1);
1024         final ClassicHttpResponse result = execute(req2);
1025 
1026         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1027     }
1028 
1029     @Test
1030     public void returns504ForStaleEntryWithOnlyIfCached() throws Exception {
1031         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1032         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1033         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1034         resp1.setHeader("Cache-Control","max-age=5");
1035 
1036         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1037 
1038         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1039         req2.setHeader("Cache-Control", "only-if-cached");
1040 
1041         execute(req1);
1042         final ClassicHttpResponse result = execute(req2);
1043 
1044         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1045     }
1046 
1047     @Test
1048     public void returnsStaleCacheEntryWithOnlyIfCachedAndMaxStale() throws Exception {
1049 
1050         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1051         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1052         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1053         resp1.setHeader("Cache-Control","max-age=5");
1054 
1055         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1056 
1057         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1058         req2.setHeader("Cache-Control", "max-stale=20, only-if-cached");
1059 
1060         execute(req1);
1061         final ClassicHttpResponse result = execute(req2);
1062 
1063         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1064     }
1065 
1066     @Test
1067     public void issues304EvenWithWeakETag() throws Exception {
1068         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1069         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1070         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1071         resp1.setHeader("Cache-Control", "max-age=300");
1072         resp1.setHeader("ETag","W/\"weak-sauce\"");
1073 
1074         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1075 
1076         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1077         req2.setHeader("If-None-Match","W/\"weak-sauce\"");
1078 
1079         execute(req1);
1080         final ClassicHttpResponse result = execute(req2);
1081 
1082         assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
1083     }
1084 
1085 }