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