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