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 java.io.IOException;
30  import java.io.InputStream;
31  import java.net.SocketTimeoutException;
32  import java.util.Date;
33  import java.util.Random;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.http.Header;
38  import org.apache.http.HeaderElement;
39  import org.apache.http.HttpEntityEnclosingRequest;
40  import org.apache.http.HttpHost;
41  import org.apache.http.HttpRequest;
42  import org.apache.http.HttpResponse;
43  import org.apache.http.HttpStatus;
44  import org.apache.http.HttpVersion;
45  import org.apache.http.ProtocolVersion;
46  import org.apache.http.client.ClientProtocolException;
47  import org.apache.http.client.cache.HttpCacheEntry;
48  import org.apache.http.client.methods.CloseableHttpResponse;
49  import org.apache.http.client.methods.HttpExecutionAware;
50  import org.apache.http.client.methods.HttpRequestWrapper;
51  import org.apache.http.client.protocol.HttpClientContext;
52  import org.apache.http.client.utils.DateUtils;
53  import org.apache.http.conn.routing.HttpRoute;
54  import org.apache.http.entity.BasicHttpEntity;
55  import org.apache.http.entity.ByteArrayEntity;
56  import org.apache.http.message.BasicHeader;
57  import org.apache.http.message.BasicHttpEntityEnclosingRequest;
58  import org.apache.http.message.BasicHttpRequest;
59  import org.apache.http.message.BasicHttpResponse;
60  import org.apache.http.protocol.HTTP;
61  import org.easymock.Capture;
62  import org.easymock.EasyMock;
63  import org.junit.Assert;
64  import org.junit.Ignore;
65  import org.junit.Test;
66  
67  /**
68   * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
69   * of the rules for proxies apply to us, as far as proper operation of the
70   * requests that pass through us. Generally speaking, we want to make sure that
71   * any response returned from our HttpClient.execute() methods is conditionally
72   * compliant with the rules for an HTTP/1.1 server, and that any requests we
73   * pass downstream to the backend HttpClient are are conditionally compliant
74   * with the rules for an HTTP/1.1 client.
75   */
76  public class TestProtocolRequirements extends AbstractProtocolTest {
77  
78      @Test
79      public void testCacheMissOnGETUsesOriginResponse() throws Exception {
80          EasyMock.expect(
81                  mockBackend.execute(
82                          EasyMock.eq(route),
83                          eqRequest(request),
84                          EasyMock.isA(HttpClientContext.class),
85                          EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
86          replayMocks();
87  
88          final HttpResponse result = impl.execute(route, request, context, null);
89  
90          verifyMocks();
91          Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
92      }
93  
94      /*
95       * "Proxy and gateway applications need to be careful when forwarding
96       * messages in protocol versions different from that of the application.
97       * Since the protocol version indicates the protocol capability of the
98       * sender, a proxy/gateway MUST NOT send a message with a version indicator
99       * which is greater than its actual version. If a higher version request is
100      * received, the proxy/gateway MUST either downgrade the request version, or
101      * respond with an error, or switch to tunnel behavior."
102      *
103      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
104      */
105     @Test
106     public void testHigherMajorProtocolVersionsOnRequestSwitchToTunnelBehavior() throws Exception {
107 
108         // tunnel behavior: I don't muck with request or response in
109         // any way
110         request = HttpRequestWrapper.wrap(
111                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 2, 13)));
112 
113         EasyMock.expect(
114                 mockBackend.execute(
115                         EasyMock.eq(route),
116                         eqRequest(request),
117                         EasyMock.isA(HttpClientContext.class),
118                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
119         replayMocks();
120 
121         final HttpResponse result = impl.execute(route, request, context, null);
122 
123         verifyMocks();
124         Assert.assertSame(originResponse, result);
125     }
126 
127     @Test
128     public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
129 
130         request = HttpRequestWrapper.wrap(
131                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2)));
132 
133         final HttpRequestWrapper downgraded = HttpRequestWrapper.wrap(
134                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
135 
136         EasyMock.expect(
137                 mockBackend.execute(
138                         EasyMock.eq(route),
139                         eqRequest(downgraded),
140                         EasyMock.isA(HttpClientContext.class),
141                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
142 
143         replayMocks();
144         final HttpResponse result = impl.execute(route, request, context, null);
145 
146         verifyMocks();
147         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
148     }
149 
150     /*
151      * "Due to interoperability problems with HTTP/1.0 proxies discovered since
152      * the publication of RFC 2068[33], caching proxies MUST, gateways MAY, and
153      * tunnels MUST NOT upgrade the request to the highest version they support.
154      * The proxy/gateway's response to that request MUST be in the same major
155      * version as the request."
156      *
157      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
158      */
159     @Test
160     public void testRequestsWithLowerProtocolVersionsGetUpgradedTo1_1() throws Exception {
161 
162         request = HttpRequestWrapper.wrap(
163                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
164         final HttpRequestWrapper upgraded = HttpRequestWrapper.wrap(
165                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
166 
167         EasyMock.expect(
168                 mockBackend.execute(
169                         EasyMock.eq(route),
170                         eqRequest(upgraded),
171                         EasyMock.isA(HttpClientContext.class),
172                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
173         replayMocks();
174 
175         final HttpResponse result = impl.execute(route, request, context, null);
176 
177         verifyMocks();
178         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
179     }
180 
181     /*
182      * "An HTTP server SHOULD send a response version equal to the highest
183      * version for which the server is at least conditionally compliant, and
184      * whose major version is less than or equal to the one received in the
185      * request."
186      *
187      * http://www.ietf.org/rfc/rfc2145.txt
188      */
189     @Test
190     public void testLowerOriginResponsesUpgradedToOurVersion1_1() throws Exception {
191         originResponse = Proxies.enhanceResponse(
192                 new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 2), HttpStatus.SC_OK, "OK"));
193         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
194         originResponse.setHeader("Server", "MockOrigin/1.0");
195         originResponse.setEntity(body);
196 
197         // not testing this internal behavior in this test, just want
198         // to check the protocol version that comes out the other end
199         EasyMock.expect(
200                 mockBackend.execute(
201                         EasyMock.isA(HttpRoute.class),
202                         EasyMock.isA(HttpRequestWrapper.class),
203                         EasyMock.isA(HttpClientContext.class),
204                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
205         replayMocks();
206 
207         final HttpResponse result = impl.execute(route, request, context, null);
208 
209         verifyMocks();
210         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getProtocolVersion());
211     }
212 
213     @Test
214     public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
215         request = HttpRequestWrapper.wrap(
216                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
217 
218         EasyMock.expect(
219                 mockBackend.execute(
220                         EasyMock.isA(HttpRoute.class),
221                         EasyMock.isA(HttpRequestWrapper.class),
222                         EasyMock.isA(HttpClientContext.class),
223                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
224         replayMocks();
225 
226         final HttpResponse result = impl.execute(route, request, context, null);
227 
228         verifyMocks();
229         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getProtocolVersion());
230     }
231 
232     /*
233      * "A proxy MUST forward an unknown header, unless it is protected by a
234      * Connection header." http://www.ietf.org/rfc/rfc2145.txt
235      */
236     @Test
237     public void testForwardsUnknownHeadersOnRequestsFromHigherProtocolVersions() throws Exception {
238         request = HttpRequestWrapper.wrap(
239                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2)));
240         request.removeHeaders("Connection");
241         request.addHeader("X-Unknown-Header", "some-value");
242 
243         final HttpRequestWrapper downgraded = HttpRequestWrapper.wrap(
244                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
245         downgraded.removeHeaders("Connection");
246         downgraded.addHeader("X-Unknown-Header", "some-value");
247 
248         EasyMock.expect(
249                 mockBackend.execute(
250                         EasyMock.isA(HttpRoute.class),
251                         eqRequest(downgraded),
252                         EasyMock.isA(HttpClientContext.class),
253                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
254         replayMocks();
255 
256         impl.execute(route, request, context, null);
257 
258         verifyMocks();
259     }
260 
261     /*
262      * "A server MUST NOT send transfer-codings to an HTTP/1.0 client."
263      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
264      */
265     @Test
266     public void testTransferCodingsAreNotSentToAnHTTP_1_0Client() throws Exception {
267 
268         originResponse.setHeader("Transfer-Encoding", "identity");
269 
270         request = HttpRequestWrapper.wrap(
271                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
272 
273         EasyMock.expect(
274                 mockBackend.execute(
275                         EasyMock.isA(HttpRoute.class),
276                         EasyMock.isA(HttpRequestWrapper.class),
277                         EasyMock.isA(HttpClientContext.class),
278                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
279         replayMocks();
280 
281         final HttpResponse result = impl.execute(route, request, context, null);
282 
283         verifyMocks();
284 
285         Assert.assertNull(result.getFirstHeader("TE"));
286         Assert.assertNull(result.getFirstHeader("Transfer-Encoding"));
287     }
288 
289     /*
290      * "Multiple message-header fields with the same field-name MAY be present
291      * in a message if and only if the entire field-value for that header field
292      * is defined as a comma-separated list [i.e., #(values)]. It MUST be
293      * possible to combine the multiple header fields into one
294      * "field-name: field-value" pair, without changing the semantics of the
295      * message, by appending each subsequent field-value to the first, each
296      * separated by a comma. The order in which header fields with the same
297      * field-name are received is therefore significant to the interpretation of
298      * the combined field value, and thus a proxy MUST NOT change the order of
299      * these field values when a message is forwarded."
300      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
301      */
302     private void testOrderOfMultipleHeadersIsPreservedOnRequests(final String h, final HttpRequestWrapper request)
303             throws Exception {
304         final Capture<HttpRequestWrapper> reqCapture = new Capture<HttpRequestWrapper>();
305 
306         EasyMock.expect(
307                 mockBackend.execute(
308                         EasyMock.isA(HttpRoute.class),
309                         EasyMock.capture(reqCapture),
310                         EasyMock.isA(HttpClientContext.class),
311                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
312         replayMocks();
313 
314         impl.execute(route, request, context, null);
315 
316         verifyMocks();
317 
318         final HttpRequest forwarded = reqCapture.getValue();
319         Assert.assertNotNull(forwarded);
320         final String expected = HttpTestUtils.getCanonicalHeaderValue(request, h);
321         final String actual = HttpTestUtils.getCanonicalHeaderValue(forwarded, h);
322         if (!actual.contains(expected)) {
323             Assert.assertEquals(expected, actual);
324         }
325 
326     }
327 
328     @Test
329     public void testOrderOfMultipleAcceptHeaderValuesIsPreservedOnRequests() throws Exception {
330         request.addHeader("Accept", "audio/*; q=0.2, audio/basic");
331         request.addHeader("Accept", "text/*, text/html, text/html;level=1, */*");
332         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept", request);
333     }
334 
335     @Test
336     public void testOrderOfMultipleAcceptCharsetHeadersIsPreservedOnRequests() throws Exception {
337         request.addHeader("Accept-Charset", "iso-8859-5");
338         request.addHeader("Accept-Charset", "unicode-1-1;q=0.8");
339         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Charset", request);
340     }
341 
342     @Test
343     public void testOrderOfMultipleAcceptEncodingHeadersIsPreservedOnRequests() throws Exception {
344         request.addHeader("Accept-Encoding", "identity");
345         request.addHeader("Accept-Encoding", "compress, gzip");
346         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
347     }
348 
349     @Test
350     public void testOrderOfMultipleAcceptLanguageHeadersIsPreservedOnRequests() throws Exception {
351         request.addHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7");
352         request.addHeader("Accept-Language", "i-cherokee");
353         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
354     }
355 
356     @Test
357     public void testOrderOfMultipleAllowHeadersIsPreservedOnRequests() throws Exception {
358         final BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
359                 HttpVersion.HTTP_1_1);
360         put.setEntity(body);
361         put.addHeader("Allow", "GET, HEAD");
362         put.addHeader("Allow", "DELETE");
363         put.addHeader("Content-Length", "128");
364         testOrderOfMultipleHeadersIsPreservedOnRequests("Allow", HttpRequestWrapper.wrap(put));
365     }
366 
367     @Test
368     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnRequests() throws Exception {
369         request.addHeader("Cache-Control", "max-age=5");
370         request.addHeader("Cache-Control", "min-fresh=10");
371         testOrderOfMultipleHeadersIsPreservedOnRequests("Cache-Control", request);
372     }
373 
374     @Test
375     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnRequests() throws Exception {
376         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
377                 HttpVersion.HTTP_1_1);
378         post.setEntity(body);
379         post.addHeader("Content-Encoding", "gzip");
380         post.addHeader("Content-Encoding", "compress");
381         post.addHeader("Content-Length", "128");
382         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Encoding", HttpRequestWrapper.wrap(post));
383     }
384 
385     @Test
386     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnRequests() throws Exception {
387         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
388                 HttpVersion.HTTP_1_1);
389         post.setEntity(body);
390         post.addHeader("Content-Language", "mi");
391         post.addHeader("Content-Language", "en");
392         post.addHeader("Content-Length", "128");
393         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Language", HttpRequestWrapper.wrap(post));
394     }
395 
396     @Test
397     public void testOrderOfMultipleExpectHeadersIsPreservedOnRequests() throws Exception {
398         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
399                 HttpVersion.HTTP_1_1);
400         post.setEntity(body);
401         post.addHeader("Expect", "100-continue");
402         post.addHeader("Expect", "x-expect=true");
403         post.addHeader("Content-Length", "128");
404         testOrderOfMultipleHeadersIsPreservedOnRequests("Expect", HttpRequestWrapper.wrap(post));
405     }
406 
407     @Test
408     public void testOrderOfMultiplePragmaHeadersIsPreservedOnRequests() throws Exception {
409         request.addHeader("Pragma", "no-cache");
410         request.addHeader("Pragma", "x-pragma-1, x-pragma-2");
411         testOrderOfMultipleHeadersIsPreservedOnRequests("Pragma", request);
412     }
413 
414     @Test
415     public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
416         request.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
417         request.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
418         testOrderOfMultipleHeadersIsPreservedOnRequests("Via", request);
419     }
420 
421     @Test
422     public void testOrderOfMultipleWarningHeadersIsPreservedOnRequests() throws Exception {
423         request.addHeader("Warning", "199 fred \"bargle\"");
424         request.addHeader("Warning", "199 barney \"bungle\"");
425         testOrderOfMultipleHeadersIsPreservedOnRequests("Warning", request);
426     }
427 
428     private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
429         EasyMock.expect(
430                 mockBackend.execute(
431                         EasyMock.isA(HttpRoute.class),
432                         EasyMock.isA(HttpRequestWrapper.class),
433                         EasyMock.isA(HttpClientContext.class),
434                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
435         replayMocks();
436 
437         final HttpResponse result = impl.execute(route, request, context, null);
438 
439         verifyMocks();
440 
441         Assert.assertNotNull(result);
442         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
443                 .getCanonicalHeaderValue(result, h));
444 
445     }
446 
447     @Test
448     public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
449         originResponse = Proxies.enhanceResponse(
450                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 405, "Method Not Allowed"));
451         originResponse.addHeader("Allow", "HEAD");
452         originResponse.addHeader("Allow", "DELETE");
453         testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
454     }
455 
456     @Test
457     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
458         originResponse.addHeader("Cache-Control", "max-age=0");
459         originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
460         testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
461     }
462 
463     @Test
464     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
465         originResponse.addHeader("Content-Encoding", "gzip");
466         originResponse.addHeader("Content-Encoding", "compress");
467         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
468     }
469 
470     @Test
471     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
472         originResponse.addHeader("Content-Language", "mi");
473         originResponse.addHeader("Content-Language", "en");
474         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
475     }
476 
477     @Test
478     public void testOrderOfMultiplePragmaHeadersIsPreservedOnResponses() throws Exception {
479         originResponse.addHeader("Pragma", "no-cache, x-pragma-2");
480         originResponse.addHeader("Pragma", "x-pragma-1");
481         testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
482     }
483 
484     @Test
485     public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
486         originResponse.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
487         originResponse.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
488         testOrderOfMultipleHeadersIsPreservedOnResponses("Via");
489     }
490 
491     @Test
492     public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
493         originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
494         originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
495         testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
496     }
497 
498     /*
499      * "However, applications MUST understand the class of any status code, as
500      * indicated by the first digit, and treat any unrecognized response as
501      * being equivalent to the x00 status code of that class, with the exception
502      * that an unrecognized response MUST NOT be cached."
503      *
504      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
505      */
506     private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
507 
508         emptyMockCacheExpectsNoPuts();
509 
510         originResponse = Proxies.enhanceResponse(
511                 new BasicHttpResponse(HttpVersion.HTTP_1_1, code, "Moo"));
512         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
513         originResponse.setHeader("Server", "MockOrigin/1.0");
514         originResponse.setHeader("Cache-Control", "max-age=3600");
515         originResponse.setEntity(body);
516 
517         EasyMock.expect(
518                 mockBackend.execute(
519                         EasyMock.isA(HttpRoute.class),
520                         EasyMock.isA(HttpRequestWrapper.class),
521                         EasyMock.isA(HttpClientContext.class),
522                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
523 
524         replayMocks();
525 
526         impl.execute(route, request, context, null);
527 
528         // in particular, there were no storage calls on the cache
529         verifyMocks();
530     }
531 
532     @Test
533     public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
534         for (int i = 102; i <= 199; i++) {
535             testUnknownResponseStatusCodeIsNotCached(i);
536         }
537         for (int i = 207; i <= 299; i++) {
538             testUnknownResponseStatusCodeIsNotCached(i);
539         }
540         for (int i = 308; i <= 399; i++) {
541             testUnknownResponseStatusCodeIsNotCached(i);
542         }
543         for (int i = 418; i <= 499; i++) {
544             testUnknownResponseStatusCodeIsNotCached(i);
545         }
546         for (int i = 506; i <= 999; i++) {
547             testUnknownResponseStatusCodeIsNotCached(i);
548         }
549     }
550 
551     /*
552      * "Unrecognized header fields SHOULD be ignored by the recipient and MUST
553      * be forwarded by transparent proxies."
554      *
555      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
556      */
557     @Test
558     public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
559         request.addHeader("X-Unknown-Header", "blahblah");
560         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
561         EasyMock.expect(
562                 mockBackend.execute(
563                         EasyMock.isA(HttpRoute.class),
564                         EasyMock.capture(reqCap),
565                         EasyMock.isA(HttpClientContext.class),
566                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
567 
568         replayMocks();
569 
570         impl.execute(route, request, context, null);
571 
572         verifyMocks();
573         final HttpRequest forwarded = reqCap.getValue();
574         final Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
575         Assert.assertEquals(1, hdrs.length);
576         Assert.assertEquals("blahblah", hdrs[0].getValue());
577     }
578 
579     @Test
580     public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
581         originResponse.addHeader("X-Unknown-Header", "blahblah");
582         EasyMock.expect(
583                 mockBackend.execute(
584                         EasyMock.isA(HttpRoute.class),
585                         EasyMock.isA(HttpRequestWrapper.class),
586                         EasyMock.isA(HttpClientContext.class),
587                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
588 
589         replayMocks();
590 
591         final HttpResponse result = impl.execute(route, request, context, null);
592 
593         verifyMocks();
594         final Header[] hdrs = result.getHeaders("X-Unknown-Header");
595         Assert.assertEquals(1, hdrs.length);
596         Assert.assertEquals("blahblah", hdrs[0].getValue());
597     }
598 
599     /*
600      * "If a client will wait for a 100 (Continue) response before sending the
601      * request body, it MUST send an Expect request-header field (section 14.20)
602      * with the '100-continue' expectation."
603      *
604      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
605      */
606     @Test
607     public void testRequestsExpecting100ContinueBehaviorShouldSetExpectHeader() throws Exception {
608         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest(
609                 "POST", "/", HttpVersion.HTTP_1_1);
610         post.setHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
611         post.setHeader("Content-Length", "128");
612         post.setEntity(new BasicHttpEntity());
613 
614         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
615 
616         EasyMock.expect(
617                 mockBackend.execute(
618                         EasyMock.eq(route),
619                         EasyMock.capture(reqCap),
620                         EasyMock.isA(HttpClientContext.class),
621                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
622 
623         replayMocks();
624 
625         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
626 
627         verifyMocks();
628 
629         final HttpRequestWrapper forwarded = reqCap.getValue();
630         boolean foundExpect = false;
631         for (final Header h : forwarded.getHeaders("Expect")) {
632             for (final HeaderElement elt : h.getElements()) {
633                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
634                     foundExpect = true;
635                     break;
636                 }
637             }
638         }
639         Assert.assertTrue(foundExpect);
640     }
641 
642     /*
643      * "If a client will wait for a 100 (Continue) response before sending the
644      * request body, it MUST send an Expect request-header field (section 14.20)
645      * with the '100-continue' expectation."
646      *
647      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
648      */
649     @Test
650     public void testRequestsNotExpecting100ContinueBehaviorShouldNotSetExpectContinueHeader()
651             throws Exception {
652         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest(
653                 "POST", "/", HttpVersion.HTTP_1_1);
654         post.setHeader("Content-Length", "128");
655         post.setEntity(new BasicHttpEntity());
656 
657         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
658 
659         EasyMock.expect(
660                 mockBackend.execute(
661                         EasyMock.eq(route),
662                         EasyMock.capture(reqCap),
663                         EasyMock.isA(HttpClientContext.class),
664                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
665 
666         replayMocks();
667 
668         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
669 
670         verifyMocks();
671 
672         final HttpRequestWrapper forwarded = reqCap.getValue();
673         boolean foundExpect = false;
674         for (final Header h : forwarded.getHeaders("Expect")) {
675             for (final HeaderElement elt : h.getElements()) {
676                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
677                     foundExpect = true;
678                     break;
679                 }
680             }
681         }
682         Assert.assertFalse(foundExpect);
683     }
684 
685     /*
686      * "A client MUST NOT send an Expect request-header field (section 14.20)
687      * with the '100-continue' expectation if it does not intend to send a
688      * request body."
689      *
690      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
691      */
692     @Test
693     public void testExpect100ContinueIsNotSentIfThereIsNoRequestBody() throws Exception {
694         request.addHeader("Expect", "100-continue");
695         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
696         EasyMock.expect(
697                 mockBackend.execute(
698                         EasyMock.eq(route),
699                         EasyMock.capture(reqCap),
700                         EasyMock.isA(HttpClientContext.class),
701                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
702 
703         replayMocks();
704         impl.execute(route, request, context, null);
705         verifyMocks();
706         final HttpRequest forwarded = reqCap.getValue();
707         boolean foundExpectContinue = false;
708 
709         for (final Header h : forwarded.getHeaders("Expect")) {
710             for (final HeaderElement elt : h.getElements()) {
711                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
712                     foundExpectContinue = true;
713                     break;
714                 }
715             }
716         }
717         Assert.assertFalse(foundExpectContinue);
718     }
719 
720     /*
721      * "If a proxy receives a request that includes an Expect request- header
722      * field with the '100-continue' expectation, and the proxy either knows
723      * that the next-hop server complies with HTTP/1.1 or higher, or does not
724      * know the HTTP version of the next-hop server, it MUST forward the
725      * request, including the Expect header field.
726      *
727      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
728      */
729     @Test
730     public void testExpectHeadersAreForwardedOnRequests() throws Exception {
731         // This would mostly apply to us if we were part of an
732         // application that was a proxy, and would be the
733         // responsibility of the greater application. Our
734         // responsibility is to make sure that if we get an
735         // entity-enclosing request that we properly set (or unset)
736         // the Expect header per the request.expectContinue() flag,
737         // which is tested by the previous few tests.
738     }
739 
740     /*
741      * "A proxy MUST NOT forward a 100 (Continue) response if the request
742      * message was received from an HTTP/1.0 (or earlier) client and did not
743      * include an Expect request-header field with the '100-continue'
744      * expectation. This requirement overrides the general rule for forwarding
745      * of 1xx responses (see section 10.1)."
746      *
747      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
748      */
749     @Test
750     public void test100ContinueResponsesAreNotForwardedTo1_0ClientsWhoDidNotAskForThem()
751             throws Exception {
752 
753         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
754                 new ProtocolVersion("HTTP", 1, 0));
755         post.setEntity(body);
756         post.setHeader("Content-Length", "128");
757 
758         originResponse = Proxies.enhanceResponse(
759                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 100, "Continue"));
760         EasyMock.expect(
761                 mockBackend.execute(
762                         EasyMock.eq(route),
763                         EasyMock.isA(HttpRequestWrapper.class),
764                         EasyMock.isA(HttpClientContext.class),
765                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
766         replayMocks();
767 
768         try {
769             // if a 100 response gets up to us from the HttpClient
770             // backend, we can't really handle it at that point
771             impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
772             Assert.fail("should have thrown an exception");
773         } catch (final ClientProtocolException expected) {
774         }
775 
776         verifyMocks();
777     }
778 
779     /*
780      * "9.2 OPTIONS. ...Responses to this method are not cacheable.
781      *
782      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
783      */
784     @Test
785     public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
786         emptyMockCacheExpectsNoPuts();
787         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
788         originResponse.addHeader("Cache-Control", "max-age=3600");
789 
790         EasyMock.expect(
791                 mockBackend.execute(
792                         EasyMock.eq(route),
793                         EasyMock.isA(HttpRequestWrapper.class),
794                         EasyMock.isA(HttpClientContext.class),
795                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
796 
797         replayMocks();
798 
799         impl.execute(route, request, context, null);
800 
801         verifyMocks();
802     }
803 
804     /*
805      * "A 200 response SHOULD .... If no response body is included, the response
806      * MUST include a Content-Length field with a field-value of '0'."
807      *
808      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
809      */
810     @Test
811     public void test200ResponseToOPTIONSWithNoBodyShouldIncludeContentLengthZero() throws Exception {
812 
813         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
814         originResponse.setEntity(null);
815         originResponse.setHeader("Content-Length", "0");
816 
817         EasyMock.expect(
818                 mockBackend.execute(
819                         EasyMock.eq(route),
820                         EasyMock.isA(HttpRequestWrapper.class),
821                         EasyMock.isA(HttpClientContext.class),
822                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
823         replayMocks();
824 
825         final HttpResponse result = impl.execute(route, request, context, null);
826 
827         verifyMocks();
828         final Header contentLength = result.getFirstHeader("Content-Length");
829         Assert.assertNotNull(contentLength);
830         Assert.assertEquals("0", contentLength.getValue());
831     }
832 
833     /*
834      * "When a proxy receives an OPTIONS request on an absoluteURI for which
835      * request forwarding is permitted, the proxy MUST check for a Max-Forwards
836      * field. If the Max-Forwards field-value is zero ("0"), the proxy MUST NOT
837      * forward the message; instead, the proxy SHOULD respond with its own
838      * communication options."
839      *
840      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
841      */
842     @Test
843     public void testDoesNotForwardOPTIONSWhenMaxForwardsIsZeroOnAbsoluteURIRequest()
844             throws Exception {
845         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "*", HttpVersion.HTTP_1_1));
846         request.setHeader("Max-Forwards", "0");
847 
848         replayMocks();
849         impl.execute(route, request, context, null);
850         verifyMocks();
851     }
852 
853     /*
854      * "If the Max-Forwards field-value is an integer greater than zero, the
855      * proxy MUST decrement the field-value when it forwards the request."
856      *
857      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
858      */
859     @Test
860     public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
861 
862         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "*", HttpVersion.HTTP_1_1));
863         request.setHeader("Max-Forwards", "7");
864 
865         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
866 
867         EasyMock.expect(
868                 mockBackend.execute(
869                         EasyMock.eq(route),
870                         EasyMock.capture(cap),
871                         EasyMock.isA(HttpClientContext.class),
872                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
873 
874         replayMocks();
875         impl.execute(route, request, context, null);
876         verifyMocks();
877 
878         final HttpRequest captured = cap.getValue();
879         Assert.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
880     }
881 
882     /*
883      * "If no Max-Forwards field is present in the request, then the forwarded
884      * request MUST NOT include a Max-Forwards field."
885      *
886      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
887      */
888     @Test
889     public void testDoesNotAddAMaxForwardsHeaderToForwardedOPTIONSRequests() throws Exception {
890         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
891         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
892         EasyMock.expect(
893                 mockBackend.execute(
894                         EasyMock.eq(route),
895                         EasyMock.capture(reqCap),
896                         EasyMock.isA(HttpClientContext.class),
897                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
898 
899         replayMocks();
900         impl.execute(route, request, context, null);
901         verifyMocks();
902 
903         final HttpRequest forwarded = reqCap.getValue();
904         Assert.assertNull(forwarded.getFirstHeader("Max-Forwards"));
905     }
906 
907     /*
908      * "The HEAD method is identical to GET except that the server MUST NOT
909      * return a message-body in the response."
910      *
911      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
912      */
913     @Test
914     public void testResponseToAHEADRequestMustNotHaveABody() throws Exception {
915         request = HttpRequestWrapper.wrap(new BasicHttpRequest("HEAD", "/", HttpVersion.HTTP_1_1));
916         EasyMock.expect(
917                 mockBackend.execute(
918                         EasyMock.eq(route),
919                         EasyMock.isA(HttpRequestWrapper.class),
920                         EasyMock.isA(HttpClientContext.class),
921                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
922 
923         replayMocks();
924 
925         final HttpResponse result = impl.execute(route, request, context, null);
926 
927         verifyMocks();
928 
929         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
930     }
931 
932     /*
933      * "If the new field values indicate that the cached entity differs from the
934      * current entity (as would be indicated by a change in Content-Length,
935      * Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache
936      * entry as stale."
937      *
938      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
939      */
940     private void testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale(final String eHeader,
941             final String oldVal, final String newVal) throws Exception {
942 
943         // put something cacheable in the cache
944         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
945                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
946         final HttpResponse resp1 = HttpTestUtils.make200Response();
947         resp1.addHeader("Cache-Control", "max-age=3600");
948         resp1.setHeader(eHeader, oldVal);
949 
950         // get a head that penetrates the cache
951         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
952                 new BasicHttpRequest("HEAD", "/", HttpVersion.HTTP_1_1));
953         req2.addHeader("Cache-Control", "no-cache");
954         final HttpResponse resp2 = HttpTestUtils.make200Response();
955         resp2.setEntity(null);
956         resp2.setHeader(eHeader, newVal);
957 
958         // next request doesn't tolerate stale entry
959         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
960                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
961         req3.addHeader("Cache-Control", "max-stale=0");
962         final HttpResponse resp3 = HttpTestUtils.make200Response();
963         resp3.setHeader(eHeader, newVal);
964 
965         EasyMock.expect(
966                 mockBackend.execute(
967                         EasyMock.eq(route),
968                         eqRequest(req1),
969                         EasyMock.isA(HttpClientContext.class),
970                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
971         EasyMock.expect(
972                 mockBackend.execute(
973                         EasyMock.eq(route),
974                         eqRequest(req2),
975                         EasyMock.isA(HttpClientContext.class),
976                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
977         EasyMock.expect(
978                 mockBackend.execute(
979                         EasyMock.eq(route),
980                         EasyMock.isA(HttpRequestWrapper.class),
981                         EasyMock.isA(HttpClientContext.class),
982                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
983                                 Proxies.enhanceResponse(resp3));
984 
985         replayMocks();
986 
987         impl.execute(route, req1, context, null);
988         impl.execute(route, req2, context, null);
989         impl.execute(route, req3, context, null);
990 
991         verifyMocks();
992     }
993 
994     @Test
995     public void testHEADResponseWithUpdatedContentLengthFieldMakeACacheEntryStale()
996             throws Exception {
997         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-Length", "128", "127");
998     }
999 
1000     @Test
1001     public void testHEADResponseWithUpdatedContentMD5FieldMakeACacheEntryStale() throws Exception {
1002         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-MD5",
1003                 "Q2hlY2sgSW50ZWdyaXR5IQ==", "Q2hlY2sgSW50ZWdyaXR5IR==");
1004 
1005     }
1006 
1007     @Test
1008     public void testHEADResponseWithUpdatedETagFieldMakeACacheEntryStale() throws Exception {
1009         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("ETag", "\"etag1\"",
1010                 "\"etag2\"");
1011     }
1012 
1013     @Test
1014     public void testHEADResponseWithUpdatedLastModifiedFieldMakeACacheEntryStale() throws Exception {
1015         final Date now = new Date();
1016         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
1017         final Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L);
1018         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils
1019                 .formatDate(tenSecondsAgo), DateUtils.formatDate(sixSecondsAgo));
1020     }
1021 
1022     /*
1023      * "9.5 POST. Responses to this method are not cacheable, unless the
1024      * response includes appropriate Cache-Control or Expires header fields."
1025      *
1026      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
1027      */
1028     @Test
1029     public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
1030         emptyMockCacheExpectsNoPuts();
1031 
1032         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
1033                 HttpVersion.HTTP_1_1);
1034         post.setHeader("Content-Length", "128");
1035         post.setEntity(HttpTestUtils.makeBody(128));
1036 
1037         originResponse.removeHeaders("Cache-Control");
1038         originResponse.removeHeaders("Expires");
1039 
1040         EasyMock.expect(
1041                 mockBackend.execute(
1042                         EasyMock.isA(HttpRoute.class),
1043                         EasyMock.isA(HttpRequestWrapper.class),
1044                         EasyMock.isA(HttpClientContext.class),
1045                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1046 
1047         replayMocks();
1048 
1049         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
1050 
1051         verifyMocks();
1052     }
1053 
1054     /*
1055      * "9.5 PUT. ...Responses to this method are not cacheable."
1056      *
1057      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
1058      */
1059     @Test
1060     public void testResponsesToPUTsAreNotCached() throws Exception {
1061         emptyMockCacheExpectsNoPuts();
1062 
1063         final BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
1064                 HttpVersion.HTTP_1_1);
1065         put.setEntity(HttpTestUtils.makeBody(128));
1066         put.addHeader("Content-Length", "128");
1067 
1068         originResponse.setHeader("Cache-Control", "max-age=3600");
1069 
1070         EasyMock.expect(
1071                 mockBackend.execute(
1072                         EasyMock.isA(HttpRoute.class),
1073                         EasyMock.isA(HttpRequestWrapper.class),
1074                         EasyMock.isA(HttpClientContext.class),
1075                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1076 
1077         replayMocks();
1078 
1079         impl.execute(route, HttpRequestWrapper.wrap(put), context, null);
1080 
1081         verifyMocks();
1082     }
1083 
1084     /*
1085      * "9.6 DELETE. ... Responses to this method are not cacheable."
1086      *
1087      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
1088      */
1089     @Test
1090     public void testResponsesToDELETEsAreNotCached() throws Exception {
1091         emptyMockCacheExpectsNoPuts();
1092 
1093         request = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
1094         originResponse.setHeader("Cache-Control", "max-age=3600");
1095 
1096         EasyMock.expect(
1097                 mockBackend.execute(
1098                         EasyMock.isA(HttpRoute.class),
1099                         EasyMock.isA(HttpRequestWrapper.class),
1100                         EasyMock.isA(HttpClientContext.class),
1101                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1102 
1103         replayMocks();
1104 
1105         impl.execute(route, request, context, null);
1106 
1107         verifyMocks();
1108     }
1109 
1110     /*
1111      * "A TRACE request MUST NOT include an entity."
1112      *
1113      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
1114      */
1115     @Test
1116     public void testForwardedTRACERequestsDoNotIncludeAnEntity() throws Exception {
1117         final BasicHttpEntityEnclosingRequest trace = new BasicHttpEntityEnclosingRequest("TRACE", "/",
1118                 HttpVersion.HTTP_1_1);
1119         trace.setEntity(HttpTestUtils.makeBody(entityLength));
1120         trace.setHeader("Content-Length", Integer.toString(entityLength));
1121 
1122         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
1123 
1124         EasyMock.expect(
1125                 mockBackend.execute(
1126                         EasyMock.eq(route),
1127                         EasyMock.capture(reqCap),
1128                         EasyMock.isA(HttpClientContext.class),
1129                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1130 
1131         replayMocks();
1132         impl.execute(route, HttpRequestWrapper.wrap(trace), context, null);
1133         verifyMocks();
1134 
1135         final HttpRequest forwarded = reqCap.getValue();
1136         if (forwarded instanceof HttpEntityEnclosingRequest) {
1137             final HttpEntityEnclosingRequest bodyReq = (HttpEntityEnclosingRequest) forwarded;
1138             Assert.assertTrue(bodyReq.getEntity() == null
1139                     || bodyReq.getEntity().getContentLength() == 0);
1140         } else {
1141             // request didn't enclose an entity
1142         }
1143     }
1144 
1145     /*
1146      * "9.8 TRACE ... Responses to this method MUST NOT be cached."
1147      *
1148      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
1149      */
1150     @Test
1151     public void testResponsesToTRACEsAreNotCached() throws Exception {
1152         emptyMockCacheExpectsNoPuts();
1153 
1154         request = HttpRequestWrapper.wrap(new BasicHttpRequest("TRACE", "/", HttpVersion.HTTP_1_1));
1155         originResponse.setHeader("Cache-Control", "max-age=3600");
1156 
1157         EasyMock.expect(
1158                 mockBackend.execute(
1159                         EasyMock.isA(HttpRoute.class),
1160                         EasyMock.isA(HttpRequestWrapper.class),
1161                         EasyMock.isA(HttpClientContext.class),
1162                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1163 
1164         replayMocks();
1165 
1166         impl.execute(route, request, context, null);
1167 
1168         verifyMocks();
1169     }
1170 
1171     /*
1172      * "The 204 response MUST NOT include a message-body, and thus is always
1173      * terminated by the first empty line after the header fields."
1174      *
1175      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
1176      */
1177     @Test
1178     public void test204ResponsesDoNotContainMessageBodies() throws Exception {
1179         originResponse = Proxies.enhanceResponse(
1180                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"));
1181         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1182 
1183         EasyMock.expect(
1184                 mockBackend.execute(
1185                         EasyMock.isA(HttpRoute.class),
1186                         EasyMock.isA(HttpRequestWrapper.class),
1187                         EasyMock.isA(HttpClientContext.class),
1188                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1189 
1190         replayMocks();
1191 
1192         final HttpResponse result = impl.execute(route, request, context, null);
1193 
1194         verifyMocks();
1195 
1196         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1197     }
1198 
1199     /*
1200      * "10.2.6 205 Reset Content ... The response MUST NOT include an entity."
1201      *
1202      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.6
1203      */
1204     @Test
1205     public void test205ResponsesDoNotContainMessageBodies() throws Exception {
1206         originResponse = Proxies.enhanceResponse(
1207                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_RESET_CONTENT, "Reset Content"));
1208         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1209 
1210         EasyMock.expect(
1211                 mockBackend.execute(
1212                         EasyMock.isA(HttpRoute.class),
1213                         EasyMock.isA(HttpRequestWrapper.class),
1214                         EasyMock.isA(HttpClientContext.class),
1215                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1216 
1217         replayMocks();
1218 
1219         final HttpResponse result = impl.execute(route, request, context, null);
1220 
1221         verifyMocks();
1222 
1223         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1224     }
1225 
1226     /*
1227      * "The [206] response MUST include the following header fields:
1228      *
1229      * - Either a Content-Range header field (section 14.16) indicating the
1230      * range included with this response, or a multipart/byteranges Content-Type
1231      * including Content-Range fields for each part. If a Content-Length header
1232      * field is present in the response, its value MUST match the actual number
1233      * of OCTETs transmitted in the message-body.
1234      *
1235      * - Date
1236      *
1237      * - ETag and/or Content-Location, if the header would have been sent in a
1238      * 200 response to the same request
1239      *
1240      * - Expires, Cache-Control, and/or Vary, if the field-value might differ
1241      * from that sent in any previous response for the same variant"
1242      *
1243      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1244      */
1245     @Test
1246     public void test206ResponseGeneratedFromCacheMustHaveContentRangeOrMultipartByteRangesContentType()
1247             throws Exception {
1248 
1249         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1250                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1251         final HttpResponse resp1 = HttpTestUtils.make200Response();
1252         resp1.setHeader("ETag", "\"etag\"");
1253         resp1.setHeader("Cache-Control", "max-age=3600");
1254 
1255         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1256                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1257         req2.setHeader("Range", "bytes=0-50");
1258 
1259         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1260 
1261         replayMocks();
1262         impl.execute(route, req1, context, null);
1263         final HttpResponse result = impl.execute(route, req2, context, null);
1264         verifyMocks();
1265 
1266         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1267             if (result.getFirstHeader("Content-Range") == null) {
1268                 final HeaderElement elt = result.getFirstHeader("Content-Type").getElements()[0];
1269                 Assert.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
1270                 Assert.assertNotNull(elt.getParameterByName("boundary"));
1271                 Assert.assertNotNull(elt.getParameterByName("boundary").getValue());
1272                 Assert.assertFalse("".equals(elt.getParameterByName("boundary").getValue().trim()));
1273             }
1274         }
1275     }
1276 
1277     @Test
1278     public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent()
1279             throws Exception {
1280 
1281         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1282                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1283         final HttpResponse resp1 = HttpTestUtils.make200Response();
1284         resp1.setHeader("ETag", "\"etag\"");
1285         resp1.setHeader("Cache-Control", "max-age=3600");
1286 
1287         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1288                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1289         req2.setHeader("Range", "bytes=0-50");
1290 
1291         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1292 
1293         replayMocks();
1294         impl.execute(route, req1, context, null);
1295         final HttpResponse result = impl.execute(route, req2, context, null);
1296         verifyMocks();
1297 
1298         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1299             final Header h = result.getFirstHeader("Content-Length");
1300             if (h != null) {
1301                 final int contentLength = Integer.parseInt(h.getValue());
1302                 int bytesRead = 0;
1303                 final InputStream i = result.getEntity().getContent();
1304                 while ((i.read()) != -1) {
1305                     bytesRead++;
1306                 }
1307                 i.close();
1308                 Assert.assertEquals(contentLength, bytesRead);
1309             }
1310         }
1311     }
1312 
1313     @Test
1314     public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
1315         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1316                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1317         final HttpResponse resp1 = HttpTestUtils.make200Response();
1318         resp1.setHeader("ETag", "\"etag\"");
1319         resp1.setHeader("Cache-Control", "max-age=3600");
1320 
1321         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1322                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1323         req2.setHeader("Range", "bytes=0-50");
1324 
1325         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1326 
1327         replayMocks();
1328         impl.execute(route, req1, context, null);
1329         final HttpResponse result = impl.execute(route, req2, context, null);
1330         verifyMocks();
1331 
1332         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1333             Assert.assertNotNull(result.getFirstHeader("Date"));
1334         }
1335     }
1336 
1337     @Test
1338     public void test206ResponseReturnedToClientMustHaveDateHeader() throws Exception {
1339         request.addHeader("Range", "bytes=0-50");
1340         originResponse = Proxies.enhanceResponse(
1341                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content"));
1342         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1343         originResponse.setHeader("Server", "MockOrigin/1.0");
1344         originResponse.setEntity(HttpTestUtils.makeBody(500));
1345         originResponse.setHeader("Content-Range", "bytes 0-499/1234");
1346         originResponse.removeHeaders("Date");
1347 
1348         EasyMock.expect(
1349                 mockBackend.execute(
1350                         EasyMock.isA(HttpRoute.class),
1351                         EasyMock.isA(HttpRequestWrapper.class),
1352                         EasyMock.isA(HttpClientContext.class),
1353                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1354 
1355         replayMocks();
1356 
1357         final HttpResponse result = impl.execute(route, request, context, null);
1358         Assert.assertTrue(result.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT
1359                 || result.getFirstHeader("Date") != null);
1360 
1361         verifyMocks();
1362     }
1363 
1364     @Test
1365     public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
1366         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1367                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1368 
1369         originResponse.addHeader("Cache-Control", "max-age=3600");
1370         originResponse.addHeader("ETag", "\"etag1\"");
1371 
1372         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1373                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1374         req2.addHeader("Range", "bytes=0-50");
1375 
1376         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1377 
1378         replayMocks();
1379 
1380         impl.execute(route, req1, context, null);
1381         final HttpResponse result = impl.execute(route, req2, context, null);
1382 
1383         verifyMocks();
1384 
1385         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1386             Assert.assertNotNull(result.getFirstHeader("ETag"));
1387         }
1388     }
1389 
1390     @Test
1391     public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
1392         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1393                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1394 
1395         originResponse.addHeader("Cache-Control", "max-age=3600");
1396         originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
1397 
1398         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1399                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1400         req2.addHeader("Range", "bytes=0-50");
1401 
1402         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1403 
1404         replayMocks();
1405 
1406         impl.execute(route, req1, context, null);
1407         final HttpResponse result = impl.execute(route, req2, context, null);
1408 
1409         verifyMocks();
1410 
1411         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1412             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1413         }
1414     }
1415 
1416     @Test
1417     public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
1418 
1419         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1420                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1421         req1.addHeader("Accept-Encoding", "gzip");
1422 
1423         final Date now = new Date();
1424         final Date inOneHour = new Date(now.getTime() + 3600 * 1000L);
1425         originResponse.addHeader("Cache-Control", "max-age=3600");
1426         originResponse.addHeader("Expires", DateUtils.formatDate(inOneHour));
1427         originResponse.addHeader("Vary", "Accept-Encoding");
1428 
1429         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1430                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1431         req2.addHeader("Cache-Control", "no-cache");
1432         req2.addHeader("Accept-Encoding", "gzip");
1433         final Date nextSecond = new Date(now.getTime() + 1000L);
1434         final Date inTwoHoursPlusASec = new Date(now.getTime() + 2 * 3600 * 1000L + 1000L);
1435 
1436         final HttpResponse originResponse2 = HttpTestUtils.make200Response();
1437         originResponse2.setHeader("Date", DateUtils.formatDate(nextSecond));
1438         originResponse2.setHeader("Cache-Control", "max-age=7200");
1439         originResponse2.setHeader("Expires", DateUtils.formatDate(inTwoHoursPlusASec));
1440         originResponse2.setHeader("Vary", "Accept-Encoding");
1441 
1442         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1443                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1444         req3.addHeader("Range", "bytes=0-50");
1445         req3.addHeader("Accept-Encoding", "gzip");
1446 
1447         backendExpectsAnyRequest().andReturn(originResponse);
1448         backendExpectsAnyRequestAndReturn(originResponse2).times(1, 2);
1449 
1450         replayMocks();
1451 
1452         impl.execute(route, req1, context, null);
1453         impl.execute(route, req2, context, null);
1454         final HttpResponse result = impl.execute(route, req3, context, null);
1455 
1456         verifyMocks();
1457 
1458         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1459             Assert.assertNotNull(result.getFirstHeader("Expires"));
1460             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
1461             Assert.assertNotNull(result.getFirstHeader("Vary"));
1462         }
1463     }
1464 
1465     /*
1466      * "If the [206] response is the result of an If-Range request that used a
1467      * weak validator, the response MUST NOT include other entity-headers; this
1468      * prevents inconsistencies between cached entity-bodies and updated
1469      * headers."
1470      *
1471      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1472      */
1473     @Test
1474     public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders()
1475             throws Exception {
1476 
1477         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1478                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1479 
1480         final Date now = new Date();
1481         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1482         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
1483         originResponse.addHeader("Allow", "GET,HEAD");
1484         originResponse.addHeader("Cache-Control", "max-age=3600");
1485         originResponse.addHeader("Content-Language", "en");
1486         originResponse.addHeader("Content-Encoding", "x-coding");
1487         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1488         originResponse.addHeader("Content-Length", "128");
1489         originResponse.addHeader("Content-Type", "application/octet-stream");
1490         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1491         originResponse.addHeader("ETag", "W/\"weak-tag\"");
1492 
1493         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1494                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1495         req2.addHeader("If-Range", "W/\"weak-tag\"");
1496         req2.addHeader("Range", "bytes=0-50");
1497 
1498         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1499 
1500         replayMocks();
1501 
1502         impl.execute(route, req1, context, null);
1503         final HttpResponse result = impl.execute(route, req2, context, null);
1504 
1505         verifyMocks();
1506 
1507         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1508             Assert.assertNull(result.getFirstHeader("Allow"));
1509             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
1510             Assert.assertNull(result.getFirstHeader("Content-Language"));
1511             Assert.assertNull(result.getFirstHeader("Content-MD5"));
1512             Assert.assertNull(result.getFirstHeader("Last-Modified"));
1513         }
1514     }
1515 
1516     /*
1517      * "Otherwise, the [206] response MUST include all of the entity-headers
1518      * that would have been returned with a 200 (OK) response to the same
1519      * [If-Range] request."
1520      *
1521      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1522      */
1523     @Test
1524     public void test206ResponseToIfRangeWithStrongValidatorReturnsAllEntityHeaders()
1525             throws Exception {
1526 
1527         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1528                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1529 
1530         final Date now = new Date();
1531         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1532         originResponse.addHeader("Allow", "GET,HEAD");
1533         originResponse.addHeader("Cache-Control", "max-age=3600");
1534         originResponse.addHeader("Content-Language", "en");
1535         originResponse.addHeader("Content-Encoding", "x-coding");
1536         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1537         originResponse.addHeader("Content-Length", "128");
1538         originResponse.addHeader("Content-Type", "application/octet-stream");
1539         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1540         originResponse.addHeader("ETag", "\"strong-tag\"");
1541 
1542         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1543                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1544         req2.addHeader("If-Range", "\"strong-tag\"");
1545         req2.addHeader("Range", "bytes=0-50");
1546 
1547         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1548 
1549         replayMocks();
1550 
1551         impl.execute(route, req1, context, null);
1552         final HttpResponse result = impl.execute(route, req2, context, null);
1553 
1554         verifyMocks();
1555 
1556         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1557             Assert.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
1558             Assert.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
1559             Assert.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
1560             Assert.assertEquals("x-coding", result.getFirstHeader("Content-Encoding").getValue());
1561             Assert.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
1562                     .getValue());
1563             Assert.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
1564                     .getFirstHeader("Last-Modified").getValue());
1565         }
1566     }
1567 
1568     /*
1569      * "A cache MUST NOT combine a 206 response with other previously cached
1570      * content if the ETag or Last-Modified headers do not match exactly, see
1571      * 13.5.4."
1572      *
1573      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1574      */
1575     @Test
1576     public void test206ResponseIsNotCombinedWithPreviousContentIfETagDoesNotMatch()
1577             throws Exception {
1578 
1579         final Date now = new Date();
1580 
1581         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1582                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1583         final HttpResponse resp1 = HttpTestUtils.make200Response();
1584         resp1.setHeader("Cache-Control", "max-age=3600");
1585         resp1.setHeader("ETag", "\"etag1\"");
1586         final byte[] bytes1 = new byte[128];
1587         for (int i = 0; i < bytes1.length; i++) {
1588             bytes1[i] = (byte) 1;
1589         }
1590         resp1.setEntity(new ByteArrayEntity(bytes1));
1591 
1592         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1593                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1594         req2.setHeader("Cache-Control", "no-cache");
1595         req2.setHeader("Range", "bytes=0-50");
1596 
1597         final Date inOneSecond = new Date(now.getTime() + 1000L);
1598         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1599                 "Partial Content");
1600         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1601         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1602         resp2.setHeader("ETag", "\"etag2\"");
1603         resp2.setHeader("Content-Range", "bytes 0-50/128");
1604         final byte[] bytes2 = new byte[51];
1605         for (int i = 0; i < bytes2.length; i++) {
1606             bytes2[i] = (byte) 2;
1607         }
1608         resp2.setEntity(new ByteArrayEntity(bytes2));
1609 
1610         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1611         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1612                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1613         final HttpResponse resp3 = HttpTestUtils.make200Response();
1614         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1615         resp3.setHeader("Cache-Control", "max-age=3600");
1616         resp3.setHeader("ETag", "\"etag2\"");
1617         final byte[] bytes3 = new byte[128];
1618         for (int i = 0; i < bytes3.length; i++) {
1619             bytes3[i] = (byte) 2;
1620         }
1621         resp3.setEntity(new ByteArrayEntity(bytes3));
1622 
1623         EasyMock.expect(
1624                 mockBackend.execute(
1625                         EasyMock.isA(HttpRoute.class),
1626                         EasyMock.isA(HttpRequestWrapper.class),
1627                         EasyMock.isA(HttpClientContext.class),
1628                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1629                                 Proxies.enhanceResponse(resp1));
1630         EasyMock.expect(
1631                 mockBackend.execute(
1632                         EasyMock.isA(HttpRoute.class),
1633                         EasyMock.isA(HttpRequestWrapper.class),
1634                         EasyMock.isA(HttpClientContext.class),
1635                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1636                                 Proxies.enhanceResponse(resp2));
1637         EasyMock.expect(
1638                 mockBackend.execute(
1639                         EasyMock.isA(HttpRoute.class),
1640                         EasyMock.isA(HttpRequestWrapper.class),
1641                         EasyMock.isA(HttpClientContext.class),
1642                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1643                                 Proxies.enhanceResponse(resp3)).times(0, 1);
1644         replayMocks();
1645 
1646         impl.execute(route, req1, context, null);
1647         impl.execute(route, req2, context, null);
1648         final HttpResponse result = impl.execute(route, req3, context, null);
1649 
1650         verifyMocks();
1651 
1652         final InputStream i = result.getEntity().getContent();
1653         int b;
1654         boolean found1 = false;
1655         boolean found2 = false;
1656         while ((b = i.read()) != -1) {
1657             if (b == 1) {
1658                 found1 = true;
1659             }
1660             if (b == 2) {
1661                 found2 = true;
1662             }
1663         }
1664         i.close();
1665         Assert.assertFalse(found1 && found2); // mixture of content
1666     }
1667 
1668     @Test
1669     public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch()
1670             throws Exception {
1671 
1672         final Date now = new Date();
1673 
1674         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1675                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1676         final HttpResponse resp1 = HttpTestUtils.make200Response();
1677         final Date oneHourAgo = new Date(now.getTime() - 3600L);
1678         resp1.setHeader("Cache-Control", "max-age=3600");
1679         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1680         final byte[] bytes1 = new byte[128];
1681         for (int i = 0; i < bytes1.length; i++) {
1682             bytes1[i] = (byte) 1;
1683         }
1684         resp1.setEntity(new ByteArrayEntity(bytes1));
1685 
1686         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1687                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1688         req2.setHeader("Cache-Control", "no-cache");
1689         req2.setHeader("Range", "bytes=0-50");
1690 
1691         final Date inOneSecond = new Date(now.getTime() + 1000L);
1692         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1693                 "Partial Content");
1694         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1695         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1696         resp2.setHeader("Last-Modified", DateUtils.formatDate(now));
1697         resp2.setHeader("Content-Range", "bytes 0-50/128");
1698         final byte[] bytes2 = new byte[51];
1699         for (int i = 0; i < bytes2.length; i++) {
1700             bytes2[i] = (byte) 2;
1701         }
1702         resp2.setEntity(new ByteArrayEntity(bytes2));
1703 
1704         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1705         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1706                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1707         final HttpResponse resp3 = HttpTestUtils.make200Response();
1708         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1709         resp3.setHeader("Cache-Control", "max-age=3600");
1710         resp3.setHeader("ETag", "\"etag2\"");
1711         final byte[] bytes3 = new byte[128];
1712         for (int i = 0; i < bytes3.length; i++) {
1713             bytes3[i] = (byte) 2;
1714         }
1715         resp3.setEntity(new ByteArrayEntity(bytes3));
1716 
1717         EasyMock.expect(
1718                 mockBackend.execute(
1719                         EasyMock.isA(HttpRoute.class),
1720                         EasyMock.isA(HttpRequestWrapper.class),
1721                         EasyMock.isA(HttpClientContext.class),
1722                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1723                                 Proxies.enhanceResponse(resp1));
1724         EasyMock.expect(
1725                 mockBackend.execute(
1726                         EasyMock.isA(HttpRoute.class),
1727                         EasyMock.isA(HttpRequestWrapper.class),
1728                         EasyMock.isA(HttpClientContext.class),
1729                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1730                                 Proxies.enhanceResponse(resp2));
1731         EasyMock.expect(
1732                 mockBackend.execute(
1733                         EasyMock.isA(HttpRoute.class),
1734                         EasyMock.isA(HttpRequestWrapper.class),
1735                         EasyMock.isA(HttpClientContext.class),
1736                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1737                                 Proxies.enhanceResponse(resp3)).times(0, 1);
1738         replayMocks();
1739 
1740         impl.execute(route, req1, context, null);
1741         impl.execute(route, req2, context, null);
1742         final HttpResponse result = impl.execute(route, req3, context, null);
1743 
1744         verifyMocks();
1745 
1746         final InputStream i = result.getEntity().getContent();
1747         int b;
1748         boolean found1 = false;
1749         boolean found2 = false;
1750         while ((b = i.read()) != -1) {
1751             if (b == 1) {
1752                 found1 = true;
1753             }
1754             if (b == 2) {
1755                 found2 = true;
1756             }
1757         }
1758         i.close();
1759         Assert.assertFalse(found1 && found2); // mixture of content
1760     }
1761 
1762     /*
1763      * "A cache that does not support the Range and Content-Range headers MUST
1764      * NOT cache 206 (Partial) responses."
1765      *
1766      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1767      */
1768     @Test
1769     public void test206ResponsesAreNotCachedIfTheCacheDoesNotSupportRangeAndContentRangeHeaders()
1770             throws Exception {
1771 
1772         if (!supportsRangeAndContentRangeHeaders(impl)) {
1773             emptyMockCacheExpectsNoPuts();
1774 
1775             request = HttpRequestWrapper.wrap(
1776                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1777             request.addHeader("Range", "bytes=0-50");
1778 
1779             originResponse = Proxies.enhanceResponse(
1780                     new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1781                     "Partial Content"));
1782             originResponse.setHeader("Content-Range", "bytes 0-50/128");
1783             originResponse.setHeader("Cache-Control", "max-age=3600");
1784             final byte[] bytes = new byte[51];
1785             new Random().nextBytes(bytes);
1786             originResponse.setEntity(new ByteArrayEntity(bytes));
1787 
1788             EasyMock.expect(
1789                     mockBackend.execute(
1790                             EasyMock.isA(HttpRoute.class),
1791                             EasyMock.isA(HttpRequestWrapper.class),
1792                             EasyMock.isA(HttpClientContext.class),
1793                             EasyMock.<HttpExecutionAware>isNull())).andReturn(
1794                                     originResponse);
1795 
1796             replayMocks();
1797             impl.execute(route, request, context, null);
1798             verifyMocks();
1799         }
1800     }
1801 
1802     /*
1803      * "10.3.4 303 See Other ... The 303 response MUST NOT be cached, but the
1804      * response to the second (redirected) request might be cacheable."
1805      *
1806      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
1807      */
1808     @Test
1809     public void test303ResponsesAreNotCached() throws Exception {
1810         emptyMockCacheExpectsNoPuts();
1811 
1812         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1813 
1814         originResponse = Proxies.enhanceResponse(
1815                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_SEE_OTHER, "See Other"));
1816         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1817         originResponse.setHeader("Server", "MockServer/1.0");
1818         originResponse.setHeader("Cache-Control", "max-age=3600");
1819         originResponse.setHeader("Content-Type", "application/x-cachingclient-test");
1820         originResponse.setHeader("Location", "http://foo.example.com/other");
1821         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1822 
1823         EasyMock.expect(
1824                 mockBackend.execute(
1825                         EasyMock.isA(HttpRoute.class),
1826                         EasyMock.isA(HttpRequestWrapper.class),
1827                         EasyMock.isA(HttpClientContext.class),
1828                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1829 
1830         replayMocks();
1831         impl.execute(route, request, context, null);
1832         verifyMocks();
1833     }
1834 
1835     /*
1836      * "The 304 response MUST NOT contain a message-body, and thus is always
1837      * terminated by the first empty line after the header fields."
1838      *
1839      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1840      */
1841     @Test
1842     public void test304ResponseDoesNotContainABody() throws Exception {
1843         request.setHeader("If-None-Match", "\"etag\"");
1844 
1845         originResponse = Proxies.enhanceResponse(
1846                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
1847                         "Not Modified"));
1848         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1849         originResponse.setHeader("Server", "MockServer/1.0");
1850         originResponse.setHeader("Content-Length", "128");
1851         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1852 
1853         EasyMock.expect(
1854                 mockBackend.execute(
1855                         EasyMock.isA(HttpRoute.class),
1856                         EasyMock.isA(HttpRequestWrapper.class),
1857                         EasyMock.isA(HttpClientContext.class),
1858                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1859 
1860         replayMocks();
1861 
1862         final HttpResponse result = impl.execute(route, request, context, null);
1863 
1864         verifyMocks();
1865 
1866         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1867     }
1868 
1869     /*
1870      * "The [304] response MUST include the following header fields: - Date,
1871      * unless its omission is required by section 14.18.1 [clockless origin
1872      * servers]."
1873      *
1874      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1875      */
1876     @Test
1877     public void test304ResponseWithDateHeaderForwardedFromOriginIncludesDateHeader()
1878             throws Exception {
1879 
1880         request.setHeader("If-None-Match", "\"etag\"");
1881 
1882         originResponse = Proxies.enhanceResponse(
1883                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
1884                         "Not Modified"));
1885         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1886         originResponse.setHeader("Server", "MockServer/1.0");
1887         originResponse.setHeader("ETag", "\"etag\"");
1888 
1889         EasyMock.expect(
1890                 mockBackend.execute(
1891                         EasyMock.isA(HttpRoute.class),
1892                         EasyMock.isA(HttpRequestWrapper.class),
1893                         EasyMock.isA(HttpClientContext.class),
1894                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1895         replayMocks();
1896 
1897         final HttpResponse result = impl.execute(route, request, context, null);
1898 
1899         verifyMocks();
1900         Assert.assertNotNull(result.getFirstHeader("Date"));
1901     }
1902 
1903     @Test
1904     public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
1905 
1906         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1907                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1908         originResponse.setHeader("Cache-Control", "max-age=3600");
1909         originResponse.setHeader("ETag", "\"etag\"");
1910 
1911         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1912                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1913         req2.setHeader("If-None-Match", "\"etag\"");
1914 
1915         EasyMock.expect(
1916                 mockBackend.execute(
1917                         EasyMock.isA(HttpRoute.class),
1918                         EasyMock.isA(HttpRequestWrapper.class),
1919                         EasyMock.isA(HttpClientContext.class),
1920                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1921         replayMocks();
1922 
1923         impl.execute(route, req1, context, null);
1924         final HttpResponse result = impl.execute(route, req2, context, null);
1925 
1926         verifyMocks();
1927         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1928             Assert.assertNotNull(result.getFirstHeader("Date"));
1929         }
1930     }
1931 
1932     /*
1933      * "The [304] response MUST include the following header fields: - ETag
1934      * and/or Content-Location, if the header would have been sent in a 200
1935      * response to the same request."
1936      *
1937      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1938      */
1939     @Test
1940     public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
1941         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1942                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1943         originResponse.setHeader("Cache-Control", "max-age=3600");
1944         originResponse.setHeader("ETag", "\"etag\"");
1945 
1946         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1947                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1948         req2.setHeader("If-None-Match", "\"etag\"");
1949 
1950         EasyMock.expect(
1951                 mockBackend.execute(
1952                         EasyMock.isA(HttpRoute.class),
1953                         EasyMock.isA(HttpRequestWrapper.class),
1954                         EasyMock.isA(HttpClientContext.class),
1955                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1956         replayMocks();
1957 
1958         impl.execute(route, req1, context, null);
1959         final HttpResponse result = impl.execute(route, req2, context, null);
1960 
1961         verifyMocks();
1962         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1963             Assert.assertNotNull(result.getFirstHeader("ETag"));
1964         }
1965     }
1966 
1967     @Test
1968     public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid()
1969             throws Exception {
1970         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1971                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1972         originResponse.setHeader("Cache-Control", "max-age=3600");
1973         originResponse.setHeader("Content-Location", "http://foo.example.com/other");
1974         originResponse.setHeader("ETag", "\"etag\"");
1975 
1976         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1977                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1978         req2.setHeader("If-None-Match", "\"etag\"");
1979 
1980         EasyMock.expect(
1981                 mockBackend.execute(
1982                         EasyMock.isA(HttpRoute.class),
1983                         EasyMock.isA(HttpRequestWrapper.class),
1984                         EasyMock.isA(HttpClientContext.class),
1985                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1986         replayMocks();
1987 
1988         impl.execute(route, req1, context, null);
1989         final HttpResponse result = impl.execute(route, req2, context, null);
1990 
1991         verifyMocks();
1992         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1993             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1994         }
1995     }
1996 
1997     /*
1998      * "The [304] response MUST include the following header fields: ... -
1999      * Expires, Cache-Control, and/or Vary, if the field-value might differ from
2000      * that sent in any previous response for the same variant
2001      *
2002      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2003      */
2004     @Test
2005     public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer()
2006             throws Exception {
2007 
2008         final Date now = new Date();
2009         final Date inTwoHours = new Date(now.getTime() + 2 * 3600 * 1000L);
2010 
2011         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2012                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2013         req1.setHeader("Accept-Encoding", "gzip");
2014 
2015         final HttpResponse resp1 = HttpTestUtils.make200Response();
2016         resp1.setHeader("ETag", "\"v1\"");
2017         resp1.setHeader("Cache-Control", "max-age=7200");
2018         resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours));
2019         resp1.setHeader("Vary", "Accept-Encoding");
2020         resp1.setEntity(HttpTestUtils.makeBody(entityLength));
2021 
2022         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2023                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2024         req1.setHeader("Accept-Encoding", "gzip");
2025         req1.setHeader("Cache-Control", "no-cache");
2026 
2027         final HttpResponse resp2 = HttpTestUtils.make200Response();
2028         resp2.setHeader("ETag", "\"v2\"");
2029         resp2.setHeader("Cache-Control", "max-age=3600");
2030         resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours));
2031         resp2.setHeader("Vary", "Accept-Encoding");
2032         resp2.setEntity(HttpTestUtils.makeBody(entityLength));
2033 
2034         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
2035                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2036         req3.setHeader("Accept-Encoding", "gzip");
2037         req3.setHeader("If-None-Match", "\"v2\"");
2038 
2039         EasyMock.expect(
2040                 mockBackend.execute(
2041                         EasyMock.isA(HttpRoute.class),
2042                         EasyMock.isA(HttpRequestWrapper.class),
2043                         EasyMock.isA(HttpClientContext.class),
2044                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2045                                 Proxies.enhanceResponse(resp1));
2046         EasyMock.expect(
2047                 mockBackend.execute(
2048                         EasyMock.isA(HttpRoute.class),
2049                         EasyMock.isA(HttpRequestWrapper.class),
2050                         EasyMock.isA(HttpClientContext.class),
2051                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2052                                 Proxies.enhanceResponse(resp2)).times(1, 2);
2053         replayMocks();
2054 
2055         impl.execute(route, req1, context, null);
2056         impl.execute(route, req2, context, null);
2057         final HttpResponse result = impl.execute(route, req3, context, null);
2058 
2059         verifyMocks();
2060 
2061         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
2062             Assert.assertNotNull(result.getFirstHeader("Expires"));
2063             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
2064             Assert.assertNotNull(result.getFirstHeader("Vary"));
2065         }
2066     }
2067 
2068     /*
2069      * "Otherwise (i.e., the conditional GET used a weak validator), the
2070      * response MUST NOT include other entity-headers; this prevents
2071      * inconsistencies between cached entity-bodies and updated headers."
2072      *
2073      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2074      */
2075     @Test
2076     public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders()
2077             throws Exception {
2078 
2079         final Date now = new Date();
2080         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
2081 
2082         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2083                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2084 
2085         final HttpResponse resp1 = HttpTestUtils.make200Response();
2086         resp1.setHeader("ETag", "W/\"v1\"");
2087         resp1.setHeader("Allow", "GET,HEAD");
2088         resp1.setHeader("Content-Encoding", "x-coding");
2089         resp1.setHeader("Content-Language", "en");
2090         resp1.setHeader("Content-Length", "128");
2091         resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
2092         resp1.setHeader("Content-Type", "application/octet-stream");
2093         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
2094         resp1.setHeader("Cache-Control", "max-age=7200");
2095 
2096         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2097                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2098         req2.setHeader("If-None-Match", "W/\"v1\"");
2099 
2100         EasyMock.expect(
2101                 mockBackend.execute(
2102                         EasyMock.isA(HttpRoute.class),
2103                         EasyMock.isA(HttpRequestWrapper.class),
2104                         EasyMock.isA(HttpClientContext.class),
2105                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2106                                 Proxies.enhanceResponse(resp1)).times(1, 2);
2107         replayMocks();
2108 
2109         impl.execute(route, req1, context, null);
2110         final HttpResponse result = impl.execute(route, req2, context, null);
2111 
2112         verifyMocks();
2113 
2114         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
2115             Assert.assertNull(result.getFirstHeader("Allow"));
2116             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
2117             Assert.assertNull(result.getFirstHeader("Content-Length"));
2118             Assert.assertNull(result.getFirstHeader("Content-MD5"));
2119             Assert.assertNull(result.getFirstHeader("Content-Type"));
2120             Assert.assertNull(result.getFirstHeader("Last-Modified"));
2121         }
2122     }
2123 
2124     /*
2125      * "If a 304 response indicates an entity not currently cached, then the
2126      * cache MUST disregard the response and repeat the request without the
2127      * conditional."
2128      *
2129      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2130      */
2131     @Test
2132     public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET()
2133             throws Exception {
2134 
2135         final Date now = new Date();
2136 
2137         // load cache with cacheable entry
2138         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2139                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2140         final HttpResponse resp1 = HttpTestUtils.make200Response();
2141         resp1.setHeader("ETag", "\"etag1\"");
2142         resp1.setHeader("Cache-Control", "max-age=3600");
2143 
2144         // force a revalidation
2145         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2146                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2147         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
2148 
2149         // updated ETag provided to a conditional revalidation
2150         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2151                 "Not Modified");
2152         resp2.setHeader("Date", DateUtils.formatDate(now));
2153         resp2.setHeader("Server", "MockServer/1.0");
2154         resp2.setHeader("ETag", "\"etag2\"");
2155 
2156         // conditional validation uses If-None-Match
2157         final HttpRequestWrapper conditionalValidation = HttpRequestWrapper.wrap(
2158                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2159         conditionalValidation.setHeader("If-None-Match", "\"etag1\"");
2160 
2161         // unconditional validation doesn't use If-None-Match
2162         final HttpRequestWrapper unconditionalValidation = HttpRequestWrapper.wrap(
2163                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2164         // new response to unconditional validation provides new body
2165         final HttpResponse resp3 = HttpTestUtils.make200Response();
2166         resp1.setHeader("ETag", "\"etag2\"");
2167         resp1.setHeader("Cache-Control", "max-age=3600");
2168 
2169         EasyMock.expect(
2170                 mockBackend.execute(
2171                         EasyMock.isA(HttpRoute.class),
2172                         EasyMock.isA(HttpRequestWrapper.class),
2173                         EasyMock.isA(HttpClientContext.class),
2174                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2175                                 Proxies.enhanceResponse(resp1));
2176         // this next one will happen once if the cache tries to
2177         // conditionally validate, zero if it goes full revalidation
2178         EasyMock.expect(
2179                 mockBackend.execute(
2180                         EasyMock.eq(route),
2181                         eqRequest(conditionalValidation),
2182                         EasyMock.isA(HttpClientContext.class),
2183                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2184                                 Proxies.enhanceResponse(resp2)).times(0, 1);
2185         EasyMock.expect(
2186                 mockBackend.execute(
2187                         EasyMock.eq(route),
2188                         eqRequest(unconditionalValidation),
2189                         EasyMock.isA(HttpClientContext.class),
2190                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2191                                 Proxies.enhanceResponse(resp3));
2192         replayMocks();
2193 
2194         impl.execute(route, req1, context, null);
2195         impl.execute(route, req2, context, null);
2196 
2197         verifyMocks();
2198     }
2199 
2200     /*
2201      * "If a cache uses a received 304 response to update a cache entry, the
2202      * cache MUST update the entry to reflect any new field values given in the
2203      * response.
2204      *
2205      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2206      */
2207     @Test
2208     public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
2209 
2210         final Date now = new Date();
2211         final Date inFiveSeconds = new Date(now.getTime() + 5000L);
2212 
2213         final HttpRequestWrapper initialRequest = HttpRequestWrapper.wrap(
2214                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2215 
2216         final HttpResponse cachedResponse = HttpTestUtils.make200Response();
2217         cachedResponse.setHeader("Cache-Control", "max-age=3600");
2218         cachedResponse.setHeader("ETag", "\"etag\"");
2219 
2220         final HttpRequestWrapper secondRequest = HttpRequestWrapper.wrap(
2221                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2222         secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
2223 
2224         final HttpRequestWrapper conditionalValidationRequest = HttpRequestWrapper.wrap(
2225                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2226         conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
2227 
2228         final HttpRequestWrapper unconditionalValidationRequest = HttpRequestWrapper.wrap(
2229                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2230 
2231         // to be used if the cache generates a conditional validation
2232         final HttpResponse conditionalResponse = new BasicHttpResponse(
2233                 HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
2234         conditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
2235         conditionalResponse.setHeader("Server", "MockUtils/1.0");
2236         conditionalResponse.setHeader("ETag", "\"etag\"");
2237         conditionalResponse.setHeader("X-Extra", "junk");
2238 
2239         // to be used if the cache generates an unconditional validation
2240         final HttpResponse unconditionalResponse = HttpTestUtils.make200Response();
2241         unconditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
2242         unconditionalResponse.setHeader("ETag", "\"etag\"");
2243 
2244         final Capture<HttpRequestWrapper> cap1 = new Capture<HttpRequestWrapper>();
2245         final Capture<HttpRequestWrapper> cap2 = new Capture<HttpRequestWrapper>();
2246 
2247         EasyMock.expect(
2248                 mockBackend.execute(
2249                         EasyMock.isA(HttpRoute.class),
2250                         EasyMock.isA(HttpRequestWrapper.class),
2251                         EasyMock.isA(HttpClientContext.class),
2252                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2253                                 Proxies.enhanceResponse(cachedResponse));
2254         EasyMock.expect(
2255                 mockBackend.execute(
2256                         EasyMock.eq(route),
2257                         EasyMock.and(eqRequest(conditionalValidationRequest), EasyMock.capture(cap1)),
2258                         EasyMock.isA(HttpClientContext.class),
2259                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2260                                 Proxies.enhanceResponse(conditionalResponse)).times(0, 1);
2261         EasyMock.expect(
2262                 mockBackend.execute(
2263                         EasyMock.eq(route),
2264                         EasyMock.and(eqRequest(unconditionalValidationRequest), EasyMock.capture(cap2)),
2265                         EasyMock.isA(HttpClientContext.class),
2266                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2267                                 Proxies.enhanceResponse(unconditionalResponse)).times(0, 1);
2268 
2269         replayMocks();
2270 
2271         impl.execute(route, initialRequest, context, null);
2272         final HttpResponse result = impl.execute(route, secondRequest, context, null);
2273 
2274         verifyMocks();
2275 
2276         Assert.assertTrue((cap1.hasCaptured() && !cap2.hasCaptured())
2277                 || (!cap1.hasCaptured() && cap2.hasCaptured()));
2278 
2279         if (cap1.hasCaptured()) {
2280             Assert.assertEquals(DateUtils.formatDate(inFiveSeconds), result.getFirstHeader("Date")
2281                     .getValue());
2282             Assert.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
2283         }
2284     }
2285 
2286     /*
2287      * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
2288      * header field (section 14.47) containing a challenge applicable to the
2289      * requested resource."
2290      *
2291      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
2292      */
2293     @Test
2294     public void testMustIncludeWWWAuthenticateHeaderOnAnOrigin401Response() throws Exception {
2295         originResponse = Proxies.enhanceResponse(
2296                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 401, "Unauthorized"));
2297         originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
2298 
2299         EasyMock.expect(
2300                 mockBackend.execute(
2301                         EasyMock.isA(HttpRoute.class),
2302                         EasyMock.isA(HttpRequestWrapper.class),
2303                         EasyMock.isA(HttpClientContext.class),
2304                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2305         replayMocks();
2306 
2307         final HttpResponse result = impl.execute(route, request, context, null);
2308         if (result.getStatusLine().getStatusCode() == 401) {
2309             Assert.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
2310         }
2311 
2312         verifyMocks();
2313     }
2314 
2315     /*
2316      * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
2317      * header containing a list of valid methods for the requested resource.
2318      *
2319      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
2320      */
2321     @Test
2322     public void testMustIncludeAllowHeaderFromAnOrigin405Response() throws Exception {
2323         originResponse = Proxies.enhanceResponse(
2324                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 405, "Method Not Allowed"));
2325         originResponse.setHeader("Allow", "GET, HEAD");
2326 
2327         backendExpectsAnyRequest().andReturn(originResponse);
2328 
2329         replayMocks();
2330 
2331         final HttpResponse result = impl.execute(route, request, context, null);
2332         if (result.getStatusLine().getStatusCode() == 405) {
2333             Assert.assertNotNull(result.getFirstHeader("Allow"));
2334         }
2335 
2336         verifyMocks();
2337     }
2338 
2339     /*
2340      * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
2341      * Proxy-Authenticate header field (section 14.33) containing a challenge
2342      * applicable to the proxy for the requested resource."
2343      *
2344      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
2345      */
2346     @Test
2347     public void testMustIncludeProxyAuthenticateHeaderFromAnOrigin407Response() throws Exception {
2348         originResponse = Proxies.enhanceResponse(
2349                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 407, "Proxy Authentication Required"));
2350         originResponse.setHeader("Proxy-Authenticate", "x-scheme x-param");
2351 
2352         EasyMock.expect(
2353                 mockBackend.execute(
2354                         EasyMock.isA(HttpRoute.class),
2355                         EasyMock.isA(HttpRequestWrapper.class),
2356                         EasyMock.isA(HttpClientContext.class),
2357                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2358         replayMocks();
2359 
2360         final HttpResponse result = impl.execute(route, request, context, null);
2361         if (result.getStatusLine().getStatusCode() == 407) {
2362             Assert.assertNotNull(result.getFirstHeader("Proxy-Authenticate"));
2363         }
2364 
2365         verifyMocks();
2366     }
2367 
2368     /*
2369      * "10.4.17 416 Requested Range Not Satisfiable ... This response MUST NOT
2370      * use the multipart/byteranges content-type."
2371      *
2372      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
2373      */
2374     @Test
2375     public void testMustNotAddMultipartByteRangeContentTypeTo416Response() throws Exception {
2376         originResponse = Proxies.enhanceResponse(
2377                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 416, "Requested Range Not Satisfiable"));
2378 
2379         EasyMock.expect(
2380                 mockBackend.execute(
2381                         EasyMock.isA(HttpRoute.class),
2382                         EasyMock.isA(HttpRequestWrapper.class),
2383                         EasyMock.isA(HttpClientContext.class),
2384                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2385 
2386         replayMocks();
2387         final HttpResponse result = impl.execute(route, request, context, null);
2388         verifyMocks();
2389 
2390         if (result.getStatusLine().getStatusCode() == 416) {
2391             for (final Header h : result.getHeaders("Content-Type")) {
2392                 for (final HeaderElement elt : h.getElements()) {
2393                     Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2394                 }
2395             }
2396         }
2397     }
2398 
2399     @Test
2400     public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses()
2401             throws Exception {
2402 
2403         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
2404         originResponse.setHeader("Content-Length", "128");
2405         originResponse.setHeader("Cache-Control", "max-age=3600");
2406 
2407         final HttpRequestWrapper rangeReq = HttpRequestWrapper.wrap(
2408                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2409         rangeReq.setHeader("Range", "bytes=1000-1200");
2410 
2411         final HttpResponse orig416 = new BasicHttpResponse(HttpVersion.HTTP_1_1, 416,
2412                 "Requested Range Not Satisfiable");
2413 
2414         EasyMock.expect(
2415                 mockBackend.execute(
2416                         EasyMock.isA(HttpRoute.class),
2417                         EasyMock.isA(HttpRequestWrapper.class),
2418                         EasyMock.isA(HttpClientContext.class),
2419                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2420         // cache may 416 me right away if it understands byte ranges,
2421         // ok to delegate to origin though
2422         EasyMock.expect(
2423                 mockBackend.execute(
2424                         EasyMock.isA(HttpRoute.class),
2425                         EasyMock.isA(HttpRequestWrapper.class),
2426                         EasyMock.isA(HttpClientContext.class),
2427                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2428                                 Proxies.enhanceResponse(orig416)).times(0, 1);
2429 
2430         replayMocks();
2431         impl.execute(route, request, context, null);
2432         final HttpResponse result = impl.execute(route, rangeReq, context, null);
2433         verifyMocks();
2434 
2435         // might have gotten a 416 from the origin or the cache
2436         if (result.getStatusLine().getStatusCode() == 416) {
2437             for (final Header h : result.getHeaders("Content-Type")) {
2438                 for (final HeaderElement elt : h.getElements()) {
2439                     Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2440                 }
2441             }
2442         }
2443     }
2444 
2445     /*
2446      * "A correct cache MUST respond to a request with the most up-to-date
2447      * response held by the cache that is appropriate to the request (see
2448      * sections 13.2.5, 13.2.6, and 13.12) which meets one of the following
2449      * conditions:
2450      *
2451      * 1. It has been checked for equivalence with what the origin server would
2452      * have returned by revalidating the response with the origin server
2453      * (section 13.3);
2454      *
2455      * 2. It is "fresh enough" (see section 13.2). In the default case, this
2456      * means it meets the least restrictive freshness requirement of the client,
2457      * origin server, and cache (see section 14.9); if the origin server so
2458      * specifies, it is the freshness requirement of the origin server alone.
2459      *
2460      * If a stored response is not "fresh enough" by the most restrictive
2461      * freshness requirement of both the client and the origin server, in
2462      * carefully considered circumstances the cache MAY still return the
2463      * response with the appropriate Warning header (see section 13.1.5 and
2464      * 14.46), unless such a response is prohibited (e.g., by a "no-store"
2465      * cache-directive, or by a "no-cache" cache-request-directive; see section
2466      * 14.9).
2467      *
2468      * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect), or
2469      * error (4xx or 5xx) response message."
2470      *
2471      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2472      */
2473     @Test
2474     public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
2475 
2476         final Date now = new Date();
2477         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2478         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2479         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2480 
2481         final Header[] hdrs = new Header[] {
2482                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2483                 new BasicHeader("Cache-Control", "max-age=0"),
2484                 new BasicHeader("ETag", "\"etag\""),
2485                 new BasicHeader("Content-Length", "128")
2486         };
2487 
2488         final byte[] bytes = new byte[128];
2489         new Random().nextBytes(bytes);
2490 
2491         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2492 
2493         impl = new CachingExec(mockBackend, mockCache, config);
2494 
2495         request = HttpRequestWrapper.wrap(
2496                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2497 
2498         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2499                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2500         validate.setHeader("If-None-Match", "\"etag\"");
2501 
2502         final CloseableHttpResponse notModified = Proxies.enhanceResponse(
2503                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified"));
2504         notModified.setHeader("Date", DateUtils.formatDate(now));
2505         notModified.setHeader("ETag", "\"etag\"");
2506 
2507         EasyMock.expect(
2508                 mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request)))
2509                 .andReturn(entry);
2510         EasyMock.expect(
2511                 mockBackend.execute(
2512                         EasyMock.eq(route),
2513                         eqRequest(validate),
2514                         EasyMock.isA(HttpClientContext.class),
2515                         EasyMock.<HttpExecutionAware>isNull())).andReturn(notModified);
2516         EasyMock.expect(mockCache.updateCacheEntry(
2517                 EasyMock.eq(host),
2518                 eqRequest(request),
2519                 EasyMock.eq(entry),
2520                 eqResponse(notModified),
2521                 EasyMock.isA(Date.class),
2522                 EasyMock.isA(Date.class)))
2523             .andReturn(HttpTestUtils.makeCacheEntry());
2524 
2525         replayMocks();
2526         impl.execute(route, request, context, null);
2527         verifyMocks();
2528     }
2529 
2530     @Test
2531     public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
2532 
2533         final Date now = new Date();
2534         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2535         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2536         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2537 
2538         final Header[] hdrs = new Header[] {
2539                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2540                 new BasicHeader("Cache-Control", "max-age=3600"),
2541                 new BasicHeader("Content-Length", "128")
2542         };
2543 
2544         final byte[] bytes = new byte[128];
2545         new Random().nextBytes(bytes);
2546 
2547         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2548 
2549         impl = new CachingExec(mockBackend, mockCache, config);
2550         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2551 
2552         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2553 
2554         replayMocks();
2555         final HttpResponse result = impl.execute(route, request, context, null);
2556         verifyMocks();
2557 
2558         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2559     }
2560 
2561     /*
2562      * "If the cache can not communicate with the origin server, then a correct
2563      * cache SHOULD respond as above if the response can be correctly served
2564      * from the cache; if not it MUST return an error or warning indicating that
2565      * there was a communication failure."
2566      *
2567      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2568      *
2569      * "111 Revalidation failed MUST be included if a cache returns a stale
2570      * response because an attempt to revalidate the response failed, due to an
2571      * inability to reach the server."
2572      *
2573      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2574      */
2575     @Test
2576     public void testMustServeAppropriateErrorOrWarningIfNoOriginCommunicationPossible()
2577             throws Exception {
2578 
2579         final Date now = new Date();
2580         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2581         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2582         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2583 
2584         final Header[] hdrs = new Header[] {
2585                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2586                 new BasicHeader("Cache-Control", "max-age=0"),
2587                 new BasicHeader("Content-Length", "128"),
2588                 new BasicHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo))
2589         };
2590 
2591         final byte[] bytes = new byte[128];
2592         new Random().nextBytes(bytes);
2593 
2594         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2595 
2596         impl = new CachingExec(mockBackend, mockCache, config);
2597         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2598 
2599         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2600         EasyMock.expect(
2601                 mockBackend.execute(
2602                         EasyMock.isA(HttpRoute.class),
2603                         EasyMock.isA(HttpRequestWrapper.class),
2604                         EasyMock.isA(HttpClientContext.class),
2605                         EasyMock.<HttpExecutionAware>isNull())).andThrow(
2606                 new IOException("can't talk to origin!")).anyTimes();
2607 
2608         replayMocks();
2609 
2610         final HttpResponse result = impl.execute(route, request, context, null);
2611 
2612         verifyMocks();
2613 
2614         final int status = result.getStatusLine().getStatusCode();
2615         if (status == 200) {
2616             boolean foundWarning = false;
2617             for (final Header h : result.getHeaders("Warning")) {
2618                 if (h.getValue().split(" ")[0].equals("111")) {
2619                     foundWarning = true;
2620                 }
2621             }
2622             Assert.assertTrue(foundWarning);
2623         } else {
2624             Assert.assertTrue(status >= 500 && status <= 599);
2625         }
2626     }
2627 
2628     /*
2629      * "Whenever a cache returns a response that is neither first-hand nor
2630      * "fresh enough" (in the sense of condition 2 in section 13.1.1), it MUST
2631      * attach a warning to that effect, using a Warning general-header."
2632      *
2633      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2634      */
2635     @Test
2636     public void testAttachesWarningHeaderWhenGeneratingStaleResponse() throws Exception {
2637         // covered by previous test
2638     }
2639 
2640     /*
2641      * "1xx Warnings that describe the freshness or revalidation status of the
2642      * response, and so MUST be deleted after a successful revalidation."
2643      *
2644      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2645      */
2646     @Test
2647     public void test1xxWarningsAreDeletedAfterSuccessfulRevalidation() throws Exception {
2648 
2649         final Date now = new Date();
2650         final Date tenSecondsAgo = new Date(now.getTime() - 25 * 1000L);
2651         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2652         final HttpResponse resp1 = HttpTestUtils.make200Response();
2653         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2654         resp1.setHeader("ETag", "\"etag\"");
2655         resp1.setHeader("Cache-Control", "max-age=5");
2656         resp1.setHeader("Warning", "110 squid \"stale stuff\"");
2657         resp1.setHeader("Via", "1.1 fred");
2658 
2659         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2660 
2661         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2662                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2663         validate.setHeader("If-None-Match", "\"etag\"");
2664 
2665         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2666                 "Not Modified");
2667         resp2.setHeader("Date", DateUtils.formatDate(now));
2668         resp2.setHeader("Server", "MockServer/1.0");
2669         resp2.setHeader("ETag", "\"etag\"");
2670         resp2.setHeader("Via", "1.1 fred");
2671 
2672         backendExpectsAnyRequestAndReturn(resp1);
2673         EasyMock.expect(
2674                 mockBackend.execute(
2675                         EasyMock.eq(route),
2676                         eqRequest(validate),
2677                         EasyMock.isA(HttpClientContext.class),
2678                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2679                                 Proxies.enhanceResponse(resp2));
2680 
2681         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2682 
2683         replayMocks();
2684 
2685         final HttpResponse stale = impl.execute(route, req1, context, null);
2686         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2687 
2688         final HttpResponse result1 = impl.execute(route, req2, context, null);
2689         final HttpResponse result2 = impl.execute(route, req3, context, null);
2690 
2691         verifyMocks();
2692 
2693         boolean found1xxWarning = false;
2694         for (final Header h : result1.getHeaders("Warning")) {
2695             for (final HeaderElement elt : h.getElements()) {
2696                 if (elt.getName().startsWith("1")) {
2697                     found1xxWarning = true;
2698                 }
2699             }
2700         }
2701         for (final Header h : result2.getHeaders("Warning")) {
2702             for (final HeaderElement elt : h.getElements()) {
2703                 if (elt.getName().startsWith("1")) {
2704                     found1xxWarning = true;
2705                 }
2706             }
2707         }
2708         Assert.assertFalse(found1xxWarning);
2709     }
2710 
2711     /*
2712      * "2xx Warnings that describe some aspect of the entity body or entity
2713      * headers that is not rectified by a revalidation (for example, a lossy
2714      * compression of the entity bodies) and which MUST NOT be deleted after a
2715      * successful revalidation."
2716      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2717      */
2718     @Test
2719     public void test2xxWarningsAreNotDeletedAfterSuccessfulRevalidation() throws Exception {
2720         final Date now = new Date();
2721         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2722         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2723         final HttpResponse resp1 = HttpTestUtils.make200Response();
2724         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2725         resp1.setHeader("ETag", "\"etag\"");
2726         resp1.setHeader("Cache-Control", "max-age=5");
2727         resp1.setHeader("Via", "1.1 xproxy");
2728         resp1.setHeader("Warning", "214 xproxy \"transformed stuff\"");
2729 
2730         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2731 
2732         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2733                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2734         validate.setHeader("If-None-Match", "\"etag\"");
2735 
2736         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2737                 "Not Modified");
2738         resp2.setHeader("Date", DateUtils.formatDate(now));
2739         resp2.setHeader("Server", "MockServer/1.0");
2740         resp2.setHeader("ETag", "\"etag\"");
2741         resp1.setHeader("Via", "1.1 xproxy");
2742 
2743         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2744 
2745         backendExpectsAnyRequestAndReturn(resp1);
2746 
2747         EasyMock.expect(
2748                 mockBackend.execute(
2749                         EasyMock.eq(route),
2750                         eqRequest(validate),
2751                         EasyMock.isA(HttpClientContext.class),
2752                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2753                                 Proxies.enhanceResponse(resp2));
2754 
2755         replayMocks();
2756 
2757         final HttpResponse stale = impl.execute(route, req1, context, null);
2758         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2759 
2760         final HttpResponse result1 = impl.execute(route, req2, context, null);
2761         final HttpResponse result2 = impl.execute(route, req3, context, null);
2762 
2763         verifyMocks();
2764 
2765         boolean found214Warning = false;
2766         for (final Header h : result1.getHeaders("Warning")) {
2767             for (final HeaderElement elt : h.getElements()) {
2768                 final String[] parts = elt.getName().split(" ");
2769                 if ("214".equals(parts[0])) {
2770                     found214Warning = true;
2771                 }
2772             }
2773         }
2774         Assert.assertTrue(found214Warning);
2775 
2776         found214Warning = false;
2777         for (final Header h : result2.getHeaders("Warning")) {
2778             for (final HeaderElement elt : h.getElements()) {
2779                 final String[] parts = elt.getName().split(" ");
2780                 if ("214".equals(parts[0])) {
2781                     found214Warning = true;
2782                 }
2783             }
2784         }
2785         Assert.assertTrue(found214Warning);
2786     }
2787 
2788     /*
2789      * "When a response is generated from a cache entry, the cache MUST include
2790      * a single Age header field in the response with a value equal to the cache
2791      * entry's current_age."
2792      *
2793      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
2794      */
2795     @Test
2796     public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
2797 
2798         final Date now = new Date();
2799         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2800         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2801         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2802 
2803         final Header[] hdrs = new Header[] {
2804                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2805                 new BasicHeader("Cache-Control", "max-age=3600"),
2806                 new BasicHeader("Content-Length", "128")
2807         };
2808 
2809         final byte[] bytes = new byte[128];
2810         new Random().nextBytes(bytes);
2811 
2812         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2813 
2814         impl = new CachingExec(mockBackend, mockCache, config);
2815         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2816 
2817         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2818 
2819         replayMocks();
2820         final HttpResponse result = impl.execute(route, request, context, null);
2821         verifyMocks();
2822 
2823         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2824         Assert.assertEquals("11", result.getFirstHeader("Age").getValue());
2825     }
2826 
2827     /*
2828      * "If none of Expires, Cache-Control: max-age, or Cache-Control: s-maxage
2829      * (see section 14.9.3) appears in the response, and the response does not
2830      * include other restrictions on caching, the cache MAY compute a freshness
2831      * lifetime using a heuristic. The cache MUST attach Warning 113 to any
2832      * response whose age is more than 24 hours if such warning has not already
2833      * been added."
2834      *
2835      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4
2836      *
2837      * "113 Heuristic expiration MUST be included if the cache heuristically
2838      * chose a freshness lifetime greater than 24 hours and the response's age
2839      * is greater than 24 hours."
2840      *
2841      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2842      */
2843     @Test
2844     public void testHeuristicCacheOlderThan24HoursHasWarningAttached() throws Exception {
2845 
2846         final Date now = new Date();
2847         final Date thirtySixHoursAgo = new Date(now.getTime() - 36 * 3600 * 1000L);
2848         final Date oneYearAgo = new Date(now.getTime() - 365 * 24 * 3600 * 1000L);
2849         final Date requestTime = new Date(thirtySixHoursAgo.getTime() - 1000L);
2850         final Date responseTime = new Date(thirtySixHoursAgo.getTime() + 1000L);
2851 
2852         final Header[] hdrs = new Header[] {
2853                 new BasicHeader("Date", DateUtils.formatDate(thirtySixHoursAgo)),
2854                 new BasicHeader("Cache-Control", "public"),
2855                 new BasicHeader("Last-Modified", DateUtils.formatDate(oneYearAgo)),
2856                 new BasicHeader("Content-Length", "128")
2857         };
2858 
2859         final byte[] bytes = new byte[128];
2860         new Random().nextBytes(bytes);
2861 
2862         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(requestTime, responseTime, hdrs, bytes);
2863 
2864         impl = new CachingExec(mockBackend, mockCache, config);
2865 
2866         request = HttpRequestWrapper.wrap(
2867                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2868 
2869         final CloseableHttpResponse validated = Proxies.enhanceResponse(HttpTestUtils.make200Response());
2870         validated.setHeader("Cache-Control", "public");
2871         validated.setHeader("Last-Modified", DateUtils.formatDate(oneYearAgo));
2872         validated.setHeader("Content-Length", "128");
2873         validated.setEntity(new ByteArrayEntity(bytes));
2874 
2875         final CloseableHttpResponse reconstructed = Proxies.enhanceResponse(HttpTestUtils.make200Response());
2876 
2877         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
2878 
2879         mockCache.flushInvalidatedCacheEntriesFor(
2880                 EasyMock.isA(HttpHost.class),
2881                 EasyMock.isA(HttpRequestWrapper.class),
2882                 EasyMock.isA(HttpResponse.class));
2883         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2884         EasyMock.expect(
2885                 mockBackend.execute(
2886                         EasyMock.isA(HttpRoute.class),
2887                         EasyMock.capture(cap),
2888                         EasyMock.isA(HttpClientContext.class),
2889                         EasyMock.<HttpExecutionAware>isNull())).andReturn(validated).times(0, 1);
2890         EasyMock.expect(mockCache.getCacheEntry(
2891                 EasyMock.isA(HttpHost.class),
2892                 EasyMock.isA(HttpRequestWrapper.class))).andReturn(entry).times(0, 1);
2893         EasyMock.expect(mockCache.cacheAndReturnResponse(
2894                 EasyMock.isA(HttpHost.class),
2895                 EasyMock.isA(HttpRequestWrapper.class),
2896                 eqCloseableResponse(validated),
2897                 EasyMock.isA(Date.class),
2898                 EasyMock.isA(Date.class))).andReturn(reconstructed).times(0, 1);
2899 
2900         replayMocks();
2901         final HttpResponse result = impl.execute(route, request, context, null);
2902         verifyMocks();
2903 
2904         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2905         if (!cap.hasCaptured()) {
2906             // heuristic cache hit
2907             boolean found113Warning = false;
2908             for (final Header h : result.getHeaders("Warning")) {
2909                 for (final HeaderElement elt : h.getElements()) {
2910                     final String[] parts = elt.getName().split(" ");
2911                     if ("113".equals(parts[0])) {
2912                         found113Warning = true;
2913                         break;
2914                     }
2915                 }
2916             }
2917             Assert.assertTrue(found113Warning);
2918         }
2919     }
2920 
2921     /*
2922      * "If a cache has two fresh responses for the same representation with
2923      * different validators, it MUST use the one with the more recent Date
2924      * header."
2925      *
2926      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.5
2927      */
2928     @Test
2929     public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
2930 
2931         final Date now = new Date();
2932         final Date inFiveSecond = new Date(now.getTime() + 5 * 1000L);
2933 
2934         // put an entry in the cache
2935         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2936                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2937 
2938         final HttpResponse resp1 = HttpTestUtils.make200Response();
2939         resp1.setHeader("Date", DateUtils.formatDate(inFiveSecond));
2940         resp1.setHeader("ETag", "\"etag1\"");
2941         resp1.setHeader("Cache-Control", "max-age=3600");
2942         resp1.setHeader("Content-Length", "128");
2943 
2944         // force another origin hit
2945         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2946                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2947         req2.setHeader("Cache-Control", "no-cache");
2948 
2949         final HttpResponse resp2 = HttpTestUtils.make200Response();
2950         resp2.setHeader("Date", DateUtils.formatDate(now)); // older
2951         resp2.setHeader("ETag", "\"etag2\"");
2952         resp2.setHeader("Cache-Control", "max-age=3600");
2953         resp2.setHeader("Content-Length", "128");
2954 
2955         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
2956                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2957 
2958         EasyMock.expect(
2959                 mockBackend.execute(
2960                         EasyMock.isA(HttpRoute.class),
2961                         eqRequest(req1),
2962                         EasyMock.isA(HttpClientContext.class),
2963                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2964                                 Proxies.enhanceResponse(resp1));
2965 
2966         backendExpectsAnyRequestAndReturn(resp2);
2967 
2968         replayMocks();
2969         impl.execute(route, req1, context, null);
2970         impl.execute(route, req2, context, null);
2971         final HttpResponse result = impl.execute(route, req3, context, null);
2972         verifyMocks();
2973         Assert.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
2974     }
2975 
2976     /*
2977      * "Clients MAY issue simple (non-subrange) GET requests with either weak
2978      * validators or strong validators. Clients MUST NOT use weak validators in
2979      * other forms of request."
2980      *
2981      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
2982      *
2983      * Note that we can't determine a priori whether a given HTTP-date is a weak
2984      * or strong validator, because that might depend on an upstream client
2985      * having a cache with a Last-Modified and Date entry that allows the date
2986      * to be a strong validator. We can tell when *we* are generating a request
2987      * for validation, but we can't tell if we receive a conditional request
2988      * from upstream.
2989      */
2990     private HttpResponse testRequestWithWeakETagValidatorIsNotAllowed(final String header)
2991             throws Exception {
2992         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
2993         EasyMock.expect(
2994                 mockBackend.execute(
2995                         EasyMock.eq(route),
2996                         EasyMock.capture(cap),
2997                         EasyMock.isA(HttpClientContext.class),
2998                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(0, 1);
2999 
3000         replayMocks();
3001         final HttpResponse response = impl.execute(route, request, context, null);
3002         verifyMocks();
3003 
3004         // it's probably ok to return a 400 (Bad Request) to this client
3005         if (cap.hasCaptured()) {
3006             final HttpRequest forwarded = cap.getValue();
3007             final Header h = forwarded.getFirstHeader(header);
3008             if (h != null) {
3009                 Assert.assertFalse(h.getValue().startsWith("W/"));
3010             }
3011         }
3012         return response;
3013 
3014     }
3015 
3016     @Test
3017     public void testSubrangeGETWithWeakETagIsNotAllowed() throws Exception {
3018         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3019         request.setHeader("Range", "bytes=0-500");
3020         request.setHeader("If-Range", "W/\"etag\"");
3021 
3022         final HttpResponse response = testRequestWithWeakETagValidatorIsNotAllowed("If-Range");
3023         Assert.assertTrue(response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST);
3024     }
3025 
3026     @Test
3027     public void testPUTWithIfMatchWeakETagIsNotAllowed() throws Exception {
3028         final HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HttpVersion.HTTP_1_1);
3029         put.setEntity(HttpTestUtils.makeBody(128));
3030         put.setHeader("Content-Length", "128");
3031         put.setHeader("If-Match", "W/\"etag\"");
3032         request = HttpRequestWrapper.wrap(put);
3033 
3034         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
3035     }
3036 
3037     @Test
3038     public void testPUTWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
3039         final HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HttpVersion.HTTP_1_1);
3040         put.setEntity(HttpTestUtils.makeBody(128));
3041         put.setHeader("Content-Length", "128");
3042         put.setHeader("If-None-Match", "W/\"etag\"");
3043         request = HttpRequestWrapper.wrap(put);
3044 
3045         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
3046     }
3047 
3048     @Test
3049     public void testDELETEWithIfMatchWeakETagIsNotAllowed() throws Exception {
3050         request = HttpRequestWrapper.wrap(
3051                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
3052         request.setHeader("If-Match", "W/\"etag\"");
3053 
3054         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
3055     }
3056 
3057     @Test
3058     public void testDELETEWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
3059         request = HttpRequestWrapper.wrap(
3060                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
3061         request.setHeader("If-None-Match", "W/\"etag\"");
3062 
3063         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
3064     }
3065 
3066     /*
3067      * "A cache or origin server receiving a conditional request, other than a
3068      * full-body GET request, MUST use the strong comparison function to
3069      * evaluate the condition."
3070      *
3071      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
3072      */
3073     @Test
3074     public void testSubrangeGETMustUseStrongComparisonForCachedResponse() throws Exception {
3075         final Date now = new Date();
3076         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3077                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3078         final HttpResponse resp1 = HttpTestUtils.make200Response();
3079         resp1.setHeader("Date", DateUtils.formatDate(now));
3080         resp1.setHeader("Cache-Control", "max-age=3600");
3081         resp1.setHeader("ETag", "\"etag\"");
3082 
3083         // according to weak comparison, this would match. Strong
3084         // comparison doesn't, because the cache entry's ETag is not
3085         // marked weak. Therefore, the If-Range must fail and we must
3086         // either get an error back or the full entity, but we better
3087         // not get the conditionally-requested Partial Content (206).
3088         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3089                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3090         req2.setHeader("Range", "bytes=0-50");
3091         req2.setHeader("If-Range", "W/\"etag\"");
3092 
3093         EasyMock.expect(
3094                 mockBackend.execute(
3095                         EasyMock.isA(HttpRoute.class),
3096                         EasyMock.isA(HttpRequestWrapper.class),
3097                         EasyMock.isA(HttpClientContext.class),
3098                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3099                                 Proxies.enhanceResponse(resp1)).times(1, 2);
3100 
3101         replayMocks();
3102         impl.execute(route, req1, context, null);
3103         final HttpResponse result = impl.execute(route, req2, context, null);
3104         verifyMocks();
3105 
3106         Assert.assertFalse(HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode());
3107     }
3108 
3109     /*
3110      * "HTTP/1.1 clients: - If an entity tag has been provided by the origin
3111      * server, MUST use that entity tag in any cache-conditional request (using
3112      * If- Match or If-None-Match)."
3113      *
3114      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
3115      */
3116     @Test
3117     public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
3118 
3119         final Date now = new Date();
3120         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3121 
3122         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3123                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3124         final HttpResponse resp1 = HttpTestUtils.make200Response();
3125         resp1.setHeader("Date", DateUtils.formatDate(now));
3126         resp1.setHeader("Cache-Control", "max-age=3600");
3127         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3128         resp1.setHeader("ETag", "W/\"etag\"");
3129 
3130         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3131                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3132         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
3133 
3134         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3135         EasyMock.expect(
3136                 mockBackend.execute(
3137                         EasyMock.isA(HttpRoute.class),
3138                         EasyMock.isA(HttpRequestWrapper.class),
3139                         EasyMock.isA(HttpClientContext.class),
3140                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3141                                 Proxies.enhanceResponse(resp1));
3142 
3143         EasyMock.expect(
3144                 mockBackend.execute(
3145                         EasyMock.eq(route),
3146                         EasyMock.capture(cap),
3147                         EasyMock.isA(HttpClientContext.class),
3148                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3149                                 Proxies.enhanceResponse(resp1));
3150 
3151         replayMocks();
3152         impl.execute(route, req1, context, null);
3153         impl.execute(route, req2, context, null);
3154         verifyMocks();
3155 
3156         final HttpRequest validation = cap.getValue();
3157         boolean isConditional = false;
3158         final String[] conditionalHeaders = { "If-Range", "If-Modified-Since", "If-Unmodified-Since",
3159                 "If-Match", "If-None-Match" };
3160 
3161         for (final String ch : conditionalHeaders) {
3162             if (validation.getFirstHeader(ch) != null) {
3163                 isConditional = true;
3164                 break;
3165             }
3166         }
3167 
3168         if (isConditional) {
3169             boolean foundETag = false;
3170             for (final Header h : validation.getHeaders("If-Match")) {
3171                 for (final HeaderElement elt : h.getElements()) {
3172                     if ("W/\"etag\"".equals(elt.getName())) {
3173                         foundETag = true;
3174                     }
3175                 }
3176             }
3177             for (final Header h : validation.getHeaders("If-None-Match")) {
3178                 for (final HeaderElement elt : h.getElements()) {
3179                     if ("W/\"etag\"".equals(elt.getName())) {
3180                         foundETag = true;
3181                     }
3182                 }
3183             }
3184             Assert.assertTrue(foundETag);
3185         }
3186     }
3187 
3188     /*
3189      * "An HTTP/1.1 caching proxy, upon receiving a conditional request that
3190      * includes both a Last-Modified date and one or more entity tags as cache
3191      * validators, MUST NOT return a locally cached response to the client
3192      * unless that cached response is consistent with all of the conditional
3193      * header fields in the request."
3194      *
3195      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
3196      */
3197     @Test
3198     public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache()
3199             throws Exception {
3200         final Date now = new Date();
3201         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3202         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
3203 
3204         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3205                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3206         final HttpResponse resp1 = HttpTestUtils.make200Response();
3207         resp1.setHeader("Date", DateUtils.formatDate(now));
3208         resp1.setHeader("Cache-Control", "max-age=3600");
3209         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3210         resp1.setHeader("ETag", "W/\"etag\"");
3211 
3212         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3213                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3214         req2.setHeader("If-None-Match", "W/\"etag\"");
3215         req2.setHeader("If-Modified-Since", DateUtils.formatDate(twentySecondsAgo));
3216 
3217         // must hit the origin again for the second request
3218         EasyMock.expect(
3219                 mockBackend.execute(
3220                         EasyMock.isA(HttpRoute.class),
3221                         EasyMock.isA(HttpRequestWrapper.class),
3222                         EasyMock.isA(HttpClientContext.class),
3223                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3224                                 Proxies.enhanceResponse(resp1)).times(2);
3225 
3226         replayMocks();
3227         impl.execute(route, req1, context, null);
3228         final HttpResponse result = impl.execute(route, req2, context, null);
3229         verifyMocks();
3230 
3231         Assert.assertFalse(HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode());
3232     }
3233 
3234     @Test
3235     public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache()
3236             throws Exception {
3237         final Date now = new Date();
3238         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3239 
3240         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3241                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3242         final HttpResponse resp1 = HttpTestUtils.make200Response();
3243         resp1.setHeader("Date", DateUtils.formatDate(now));
3244         resp1.setHeader("Cache-Control", "max-age=3600");
3245         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3246         resp1.setHeader("ETag", "W/\"etag\"");
3247 
3248         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3249                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3250         req2.setHeader("If-None-Match", "W/\"etag\"");
3251         req2.setHeader("If-Modified-Since", DateUtils.formatDate(tenSecondsAgo));
3252 
3253         // may hit the origin again for the second request
3254         EasyMock.expect(
3255                 mockBackend.execute(
3256                         EasyMock.isA(HttpRoute.class),
3257                         EasyMock.isA(HttpRequestWrapper.class),
3258                         EasyMock.isA(HttpClientContext.class),
3259                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3260                                 Proxies.enhanceResponse(resp1)).times(1,2);
3261 
3262         replayMocks();
3263         impl.execute(route, req1, context, null);
3264         impl.execute(route, req2, context, null);
3265         verifyMocks();
3266     }
3267 
3268 
3269     /*
3270      * "However, a cache that does not support the Range and Content-Range
3271      * headers MUST NOT cache 206 (Partial Content) responses."
3272      *
3273      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
3274      */
3275     @Test
3276     public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response()
3277             throws Exception {
3278 
3279         if (!supportsRangeAndContentRangeHeaders(impl)) {
3280             emptyMockCacheExpectsNoPuts();
3281 
3282             final HttpRequestWrapper req = HttpRequestWrapper.wrap(
3283                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3284             req.setHeader("Range", "bytes=0-50");
3285 
3286             final HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 206, "Partial Content");
3287             resp.setHeader("Content-Range", "bytes 0-50/128");
3288             resp.setHeader("ETag", "\"etag\"");
3289             resp.setHeader("Cache-Control", "max-age=3600");
3290 
3291             EasyMock.expect(mockBackend.execute(
3292                     EasyMock.isA(HttpRoute.class),
3293                     EasyMock.isA(HttpRequestWrapper.class),
3294                     EasyMock.isA(HttpClientContext.class),
3295                     EasyMock.<HttpExecutionAware>isNull())).andReturn(Proxies.enhanceResponse(resp));
3296 
3297             replayMocks();
3298             impl.execute(route, req, context, null);
3299             verifyMocks();
3300         }
3301     }
3302 
3303     /*
3304      * "A response received with any other status code (e.g. status codes 302
3305      * and 307) MUST NOT be returned in a reply to a subsequent request unless
3306      * there are cache-control directives or another header(s) that explicitly
3307      * allow it. For example, these include the following: an Expires header
3308      * (section 14.21); a 'max-age', 's-maxage', 'must-revalidate',
3309      * 'proxy-revalidate', 'public' or 'private' cache-control directive
3310      * (section 14.9)."
3311      *
3312      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
3313      */
3314     @Test
3315     public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
3316         originResponse = Proxies.enhanceResponse(
3317                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 302, "Temporary Redirect"));
3318         originResponse.setHeader("Location", "http://foo.example.com/other");
3319         originResponse.removeHeaders("Expires");
3320         originResponse.removeHeaders("Cache-Control");
3321 
3322         backendExpectsAnyRequest().andReturn(originResponse).times(2);
3323 
3324         replayMocks();
3325         impl.execute(route, request, context, null);
3326         impl.execute(route, request, context, null);
3327         verifyMocks();
3328     }
3329 
3330     /*
3331      * "A transparent proxy MUST NOT modify any of the following fields in a
3332      * request or response, and it MUST NOT add any of these fields if not
3333      * already present: - Content-Location - Content-MD5 - ETag - Last-Modified
3334      */
3335     private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
3336         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
3337         originResponse.setHeader(header, value);
3338 
3339         backendExpectsAnyRequest().andReturn(originResponse);
3340 
3341         replayMocks();
3342         final HttpResponse result = impl.execute(route, request, context, null);
3343         verifyMocks();
3344 
3345         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3346     }
3347 
3348     @Test
3349     public void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
3350 
3351         final String url = "http://foo.example.com/other";
3352         testDoesNotModifyHeaderFromOrigin("Content-Location", url);
3353     }
3354 
3355     @Test
3356     public void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
3357         testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3358     }
3359 
3360     @Test
3361     public void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
3362         testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
3363     }
3364 
3365     @Test
3366     public void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
3367         final String lm = DateUtils.formatDate(new Date());
3368         testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
3369     }
3370 
3371     private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
3372         originResponse.removeHeaders(header);
3373 
3374         backendExpectsAnyRequest().andReturn(originResponse);
3375 
3376         replayMocks();
3377         final HttpResponse result = impl.execute(route, request, context, null);
3378         verifyMocks();
3379 
3380         Assert.assertNull(result.getFirstHeader(header));
3381     }
3382 
3383     @Test
3384     public void testDoesNotAddContentLocationToOriginResponse() throws Exception {
3385         testDoesNotAddHeaderToOriginResponse("Content-Location");
3386     }
3387 
3388     @Test
3389     public void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
3390         testDoesNotAddHeaderToOriginResponse("Content-MD5");
3391     }
3392 
3393     @Test
3394     public void testDoesNotAddEtagToOriginResponse() throws Exception {
3395         testDoesNotAddHeaderToOriginResponse("ETag");
3396     }
3397 
3398     @Test
3399     public void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
3400         testDoesNotAddHeaderToOriginResponse("Last-Modified");
3401     }
3402 
3403     private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value)
3404             throws Exception {
3405 
3406         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3407                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3408         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3409                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3410 
3411         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
3412         originResponse.setHeader("Cache-Control", "max-age=3600");
3413         originResponse.setHeader(header, value);
3414 
3415         backendExpectsAnyRequest().andReturn(originResponse);
3416 
3417         replayMocks();
3418         impl.execute(route, req1, context, null);
3419         final HttpResponse result = impl.execute(route, req2, context, null);
3420         verifyMocks();
3421 
3422         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3423     }
3424 
3425     @Test
3426     public void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
3427         final String url = "http://foo.example.com/other";
3428         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
3429     }
3430 
3431     @Test
3432     public void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
3433         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3434     }
3435 
3436     @Test
3437     public void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
3438         testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
3439     }
3440 
3441     @Test
3442     public void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
3443         final String lm = DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L));
3444         testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", lm);
3445     }
3446 
3447     private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
3448 
3449         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3450                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3451         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3452                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3453 
3454         originResponse.addHeader("Cache-Control", "max-age=3600");
3455         originResponse.removeHeaders(header);
3456 
3457         backendExpectsAnyRequest().andReturn(originResponse);
3458 
3459         replayMocks();
3460         impl.execute(route, req1, context, null);
3461         final HttpResponse result = impl.execute(route, req2, context, null);
3462         verifyMocks();
3463 
3464         Assert.assertNull(result.getFirstHeader(header));
3465     }
3466 
3467     @Test
3468     public void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
3469         testDoesNotAddHeaderOnCacheHit("Content-Location");
3470     }
3471 
3472     @Test
3473     public void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
3474         testDoesNotAddHeaderOnCacheHit("Content-MD5");
3475     }
3476 
3477     @Test
3478     public void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
3479         testDoesNotAddHeaderOnCacheHit("ETag");
3480     }
3481 
3482     @Test
3483     public void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
3484         testDoesNotAddHeaderOnCacheHit("Last-Modified");
3485     }
3486 
3487     private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
3488         final BasicHttpEntityEnclosingRequest req =
3489             new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
3490         req.setEntity(HttpTestUtils.makeBody(128));
3491         req.setHeader("Content-Length","128");
3492         req.setHeader(header,value);
3493 
3494         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3495 
3496         EasyMock.expect(
3497                 mockBackend.execute(
3498                         EasyMock.eq(route),
3499                         EasyMock.capture(cap),
3500                         EasyMock.isA(HttpClientContext.class),
3501                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
3502 
3503         replayMocks();
3504         impl.execute(route, HttpRequestWrapper.wrap(req), context, null);
3505         verifyMocks();
3506 
3507         final HttpRequest captured = cap.getValue();
3508         Assert.assertEquals(value, captured.getFirstHeader(header).getValue());
3509     }
3510 
3511     @Test
3512     public void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
3513         final String url = "http://foo.example.com/other";
3514         testDoesNotModifyHeaderOnRequest("Content-Location",url);
3515     }
3516 
3517     @Test
3518     public void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
3519         testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3520     }
3521 
3522     @Test
3523     public void testDoesNotModifyETagHeaderOnRequest() throws Exception {
3524         testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
3525     }
3526 
3527     @Test
3528     public void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
3529         final long tenSecondsAgo = System.currentTimeMillis() - 10 * 1000L;
3530         final String lm = DateUtils.formatDate(new Date(tenSecondsAgo));
3531         testDoesNotModifyHeaderOnRequest("Last-Modified", lm);
3532     }
3533 
3534     private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
3535         final BasicHttpEntityEnclosingRequest req =
3536             new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
3537         req.setEntity(HttpTestUtils.makeBody(128));
3538         req.setHeader("Content-Length","128");
3539         req.removeHeaders(header);
3540 
3541         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3542 
3543         EasyMock.expect(
3544                 mockBackend.execute(
3545                         EasyMock.eq(route),
3546                         EasyMock.capture(cap),
3547                         EasyMock.isA(HttpClientContext.class),
3548                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
3549 
3550         replayMocks();
3551         impl.execute(route, HttpRequestWrapper.wrap(req), context, null);
3552         verifyMocks();
3553 
3554         final HttpRequest captured = cap.getValue();
3555         Assert.assertNull(captured.getFirstHeader(header));
3556     }
3557 
3558     @Test
3559     public void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
3560         testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
3561     }
3562 
3563     @Test
3564     public void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
3565         testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
3566     }
3567 
3568     @Test
3569     public void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
3570         testDoesNotAddHeaderToRequestIfNotPresent("ETag");
3571     }
3572 
3573     @Test
3574     public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
3575         testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
3576     }
3577 
3578     /* " A transparent proxy MUST NOT modify any of the following
3579      * fields in a response: - Expires
3580      * but it MAY add any of these fields if not already present. If
3581      * an Expires header is added, it MUST be given a field-value
3582      * identical to that of the Date header in that response.
3583      */
3584     @Test
3585     public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
3586         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3587         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3588         testDoesNotModifyHeaderFromOrigin("Expires", expires);
3589     }
3590 
3591     @Test
3592     public void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
3593         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3594         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3595         testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", expires);
3596     }
3597 
3598     @Test
3599     public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
3600         originResponse.removeHeaders("Expires");
3601 
3602         backendExpectsAnyRequest().andReturn(originResponse);
3603 
3604         replayMocks();
3605         final HttpResponse result = impl.execute(route, request, context, null);
3606         verifyMocks();
3607 
3608         final Header expHdr = result.getFirstHeader("Expires");
3609         if (expHdr != null) {
3610             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3611                                 expHdr.getValue());
3612         }
3613     }
3614 
3615     @Test
3616     public void testExpiresHeaderMatchesDateIfAddedToCacheHit() throws Exception {
3617         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3618                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3619         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3620                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3621 
3622         originResponse.setHeader("Cache-Control","max-age=3600");
3623         originResponse.removeHeaders("Expires");
3624 
3625         backendExpectsAnyRequest().andReturn(originResponse);
3626 
3627         replayMocks();
3628         impl.execute(route, req1, context, null);
3629         final HttpResponse result = impl.execute(route, req2, context, null);
3630         verifyMocks();
3631 
3632         final Header expHdr = result.getFirstHeader("Expires");
3633         if (expHdr != null) {
3634             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3635                                 expHdr.getValue());
3636         }
3637     }
3638 
3639     /* "A proxy MUST NOT modify or add any of the following fields in
3640      * a message that contains the no-transform cache-control
3641      * directive, or in any request: - Content-Encoding - Content-Range
3642      * - Content-Type"
3643      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
3644      */
3645     private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
3646         originResponse.addHeader("Cache-Control","no-transform");
3647         originResponse.setHeader(header, value);
3648 
3649         backendExpectsAnyRequest().andReturn(originResponse);
3650 
3651         replayMocks();
3652         final HttpResponse result = impl.execute(route, request, context, null);
3653         verifyMocks();
3654 
3655         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3656     }
3657 
3658     @Test
3659     public void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
3660         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
3661     }
3662 
3663     @Test
3664     public void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
3665         request.setHeader("If-Range","\"etag\"");
3666         request.setHeader("Range","bytes=0-49");
3667 
3668         originResponse = Proxies.enhanceResponse(
3669                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 206, "Partial Content"));
3670         originResponse.setEntity(HttpTestUtils.makeBody(50));
3671         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
3672     }
3673 
3674     @Test
3675     public void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
3676         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3677     }
3678 
3679     private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
3680         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3681                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3682         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3683                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3684 
3685         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3686         originResponse.setHeader(header, value);
3687 
3688         backendExpectsAnyRequest().andReturn(originResponse);
3689 
3690         replayMocks();
3691         impl.execute(route, req1, context, null);
3692         final HttpResponse result = impl.execute(route, req2, context, null);
3693         verifyMocks();
3694 
3695         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3696     }
3697 
3698     @Test
3699     public void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
3700         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
3701     }
3702 
3703     @Test
3704     public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
3705         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3706     }
3707 
3708     @Test
3709     public void testDoesNotModifyContentRangeHeaderOnCachedResponseWithNoTransform() throws Exception {
3710         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3711                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3712         req1.setHeader("If-Range","\"etag\"");
3713         req1.setHeader("Range","bytes=0-49");
3714         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3715                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3716         req2.setHeader("If-Range","\"etag\"");
3717         req2.setHeader("Range","bytes=0-49");
3718 
3719         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3720         originResponse.setHeader("Content-Range", "bytes 0-49/128");
3721 
3722         backendExpectsAnyRequest().andReturn(originResponse).times(1,2);
3723 
3724         replayMocks();
3725         impl.execute(route, req1, context, null);
3726         final HttpResponse result = impl.execute(route, req2, context, null);
3727         verifyMocks();
3728 
3729         Assert.assertEquals("bytes 0-49/128",
3730                             result.getFirstHeader("Content-Range").getValue());
3731     }
3732 
3733     @Test
3734     public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3735         originResponse.addHeader("Cache-Control","no-transform");
3736         testDoesNotAddHeaderToOriginResponse("Content-Encoding");
3737     }
3738 
3739     @Test
3740     public void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3741         originResponse.addHeader("Cache-Control","no-transform");
3742         testDoesNotAddHeaderToOriginResponse("Content-Range");
3743     }
3744 
3745     @Test
3746     public void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3747         originResponse.addHeader("Cache-Control","no-transform");
3748         testDoesNotAddHeaderToOriginResponse("Content-Type");
3749     }
3750 
3751     /* no add on cache hit with no-transform */
3752     @Test
3753     public void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3754         originResponse.addHeader("Cache-Control","no-transform");
3755         testDoesNotAddHeaderOnCacheHit("Content-Encoding");
3756     }
3757 
3758     @Test
3759     public void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3760         originResponse.addHeader("Cache-Control","no-transform");
3761         testDoesNotAddHeaderOnCacheHit("Content-Range");
3762     }
3763 
3764     @Test
3765     public void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3766         originResponse.addHeader("Cache-Control","no-transform");
3767         testDoesNotAddHeaderOnCacheHit("Content-Type");
3768     }
3769 
3770     /* no modify on request */
3771     @Test
3772     public void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
3773         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3774     }
3775 
3776     @Test
3777     public void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
3778         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3779     }
3780 
3781     @Test
3782     public void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
3783         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3784     }
3785 
3786     @Test
3787     public void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
3788         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3789     }
3790 
3791     @Test
3792     public void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
3793         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3794     }
3795 
3796     @Test
3797     public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
3798         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3799     }
3800 
3801     /* "When a cache makes a validating request to a server, and the
3802      * server provides a 304 (Not Modified) response or a 206 (Partial
3803      * Content) response, the cache then constructs a response to send
3804      * to the requesting client.
3805      *
3806      * If the status code is 304 (Not Modified), the cache uses the
3807      * entity-body stored in the cache entry as the entity-body of
3808      * this outgoing response.
3809      *
3810      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.3
3811      */
3812     public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
3813         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3814                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3815         final HttpResponse resp1 = HttpTestUtils.make200Response();
3816         resp1.setHeader("Cache-Control","max-age=3600");
3817         resp1.setHeader("ETag","\"etag\"");
3818 
3819         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3820                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3821         req2.setHeader("Cache-Control","max-age=0, max-stale=0");
3822         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3823 
3824         backendExpectsAnyRequestAndReturn(resp1);
3825         backendExpectsAnyRequestAndReturn(resp2);
3826 
3827         replayMocks();
3828         impl.execute(route, req1, context, null);
3829         final HttpResponse result = impl.execute(route, req2, context, null);
3830         verifyMocks();
3831 
3832         final InputStream i1 = resp1.getEntity().getContent();
3833         final InputStream i2 = result.getEntity().getContent();
3834         int b1, b2;
3835         while((b1 = i1.read()) != -1) {
3836             b2 = i2.read();
3837             Assert.assertEquals(b1, b2);
3838         }
3839         b2 = i2.read();
3840         Assert.assertEquals(-1, b2);
3841         i1.close();
3842         i2.close();
3843     }
3844 
3845     /* "The end-to-end headers stored in the cache entry are used for
3846      * the constructed response, except that ...
3847      *
3848      * - any end-to-end headers provided in the 304 or 206 response MUST
3849      *  replace the corresponding headers from the cache entry.
3850      *
3851      * Unless the cache decides to remove the cache entry, it MUST
3852      * also replace the end-to-end headers stored with the cache entry
3853      * with corresponding headers received in the incoming response,
3854      * except for Warning headers as described immediately above."
3855      */
3856     private void decorateWithEndToEndHeaders(final HttpResponse r) {
3857         r.setHeader("Allow","GET");
3858         r.setHeader("Content-Encoding","gzip");
3859         r.setHeader("Content-Language","en");
3860         r.setHeader("Content-Length", "128");
3861         r.setHeader("Content-Location","http://foo.example.com/other");
3862         r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3863         r.setHeader("Content-Type", "text/html;charset=utf-8");
3864         r.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 10 * 1000L)));
3865         r.setHeader("Last-Modified", DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L)));
3866         r.setHeader("Location", "http://foo.example.com/other2");
3867         r.setHeader("Pragma", "x-pragma");
3868         r.setHeader("Retry-After","180");
3869     }
3870 
3871     @Test
3872     public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
3873         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3874                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3875         final HttpResponse resp1 = HttpTestUtils.make200Response();
3876         resp1.setHeader("Cache-Control","max-age=3600");
3877         resp1.setHeader("ETag","\"etag\"");
3878         decorateWithEndToEndHeaders(resp1);
3879 
3880         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3881                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3882         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3883         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3884         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3885         resp2.setHeader("Server", "MockServer/1.0");
3886 
3887         backendExpectsAnyRequestAndReturn(resp1);
3888         backendExpectsAnyRequestAndReturn(resp2);
3889 
3890         replayMocks();
3891         impl.execute(route, req1, context, null);
3892         final HttpResponse result = impl.execute(route, req2, context, null);
3893         verifyMocks();
3894 
3895         final String[] endToEndHeaders = {
3896             "Cache-Control", "ETag", "Allow", "Content-Encoding",
3897             "Content-Language", "Content-Length", "Content-Location",
3898             "Content-MD5", "Content-Type", "Expires", "Last-Modified",
3899             "Location", "Pragma", "Retry-After"
3900         };
3901         for(final String h : endToEndHeaders) {
3902             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
3903                                 HttpTestUtils.getCanonicalHeaderValue(result, h));
3904         }
3905     }
3906 
3907     @Test
3908     public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
3909 
3910         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3911                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3912         final HttpResponse resp1 = HttpTestUtils.make200Response();
3913         resp1.setHeader("Cache-Control","max-age=3600");
3914         resp1.setHeader("ETag","\"etag\"");
3915         decorateWithEndToEndHeaders(resp1);
3916 
3917         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3918                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3919         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3920         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3921         resp2.setHeader("Cache-Control", "max-age=1800");
3922         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3923         resp2.setHeader("Server", "MockServer/1.0");
3924         resp2.setHeader("Allow", "GET,HEAD");
3925         resp2.setHeader("Content-Language", "en,en-us");
3926         resp2.setHeader("Content-Location", "http://foo.example.com/new");
3927         resp2.setHeader("Content-Type","text/html");
3928         resp2.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 5 * 1000L)));
3929         resp2.setHeader("Location", "http://foo.example.com/new2");
3930         resp2.setHeader("Pragma","x-new-pragma");
3931         resp2.setHeader("Retry-After","120");
3932 
3933         backendExpectsAnyRequestAndReturn(resp1);
3934         backendExpectsAnyRequestAndReturn(resp2);
3935 
3936         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
3937                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3938 
3939         replayMocks();
3940         impl.execute(route, req1, context, null);
3941         final HttpResponse result1 = impl.execute(route, req2, context, null);
3942         final HttpResponse result2 = impl.execute(route, req3, context, null);
3943         verifyMocks();
3944 
3945         final String[] endToEndHeaders = {
3946             "Date", "Cache-Control", "Allow", "Content-Language",
3947             "Content-Location", "Content-Type", "Expires", "Location",
3948             "Pragma", "Retry-After"
3949         };
3950         for(final String h : endToEndHeaders) {
3951             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3952                                 HttpTestUtils.getCanonicalHeaderValue(result1, h));
3953             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3954                                 HttpTestUtils.getCanonicalHeaderValue(result2, h));
3955         }
3956     }
3957 
3958     /* "If a header field-name in the incoming response matches more
3959      * than one header in the cache entry, all such old headers MUST
3960      * be replaced."
3961      */
3962     @Test
3963     public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
3964         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3965                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3966         final HttpResponse resp1 = HttpTestUtils.make200Response();
3967         resp1.addHeader("Cache-Control","max-age=3600");
3968         resp1.addHeader("Cache-Control","public");
3969         resp1.setHeader("ETag","\"etag\"");
3970 
3971         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3972                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3973         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3974         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3975         resp2.setHeader("Cache-Control", "max-age=1800");
3976 
3977         backendExpectsAnyRequestAndReturn(resp1);
3978         backendExpectsAnyRequestAndReturn(resp2);
3979 
3980         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
3981                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3982 
3983         replayMocks();
3984         impl.execute(route, req1, context, null);
3985         final HttpResponse result1 = impl.execute(route, req2, context, null);
3986         final HttpResponse result2 = impl.execute(route, req3, context, null);
3987         verifyMocks();
3988 
3989         final String h = "Cache-Control";
3990         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3991                             HttpTestUtils.getCanonicalHeaderValue(result1, h));
3992         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3993                             HttpTestUtils.getCanonicalHeaderValue(result2, h));
3994     }
3995 
3996     /* "If a cache has a stored non-empty set of subranges for an
3997      * entity, and an incoming response transfers another subrange,
3998      * the cache MAY combine the new subrange with the existing set if
3999      * both the following conditions are met:
4000      *
4001      * - Both the incoming response and the cache entry have a cache
4002      * validator.
4003      *
4004      * - The two cache validators match using the strong comparison
4005      * function (see section 13.3.3).
4006      *
4007      * If either requirement is not met, the cache MUST use only the
4008      * most recent partial response (based on the Date values
4009      * transmitted with every response, and using the incoming
4010      * response if these values are equal or missing), and MUST
4011      * discard the other partial information."
4012      *
4013      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.4
4014      */
4015     @Test
4016     public void testCannotCombinePartialResponseIfIncomingResponseDoesNotHaveACacheValidator()
4017         throws Exception {
4018 
4019         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4020                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4021         req1.setHeader("Range","bytes=0-49");
4022 
4023         final Date now = new Date();
4024         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4025         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4026 
4027         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4028         resp1.setEntity(HttpTestUtils.makeBody(50));
4029         resp1.setHeader("Server","MockServer/1.0");
4030         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4031         resp1.setHeader("Cache-Control","max-age=3600");
4032         resp1.setHeader("Content-Range","bytes 0-49/128");
4033         resp1.setHeader("ETag","\"etag1\"");
4034 
4035         backendExpectsAnyRequestAndReturn(resp1);
4036 
4037         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4038                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4039         req2.setHeader("Range","bytes=50-127");
4040 
4041         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4042         resp2.setEntity(HttpTestUtils.makeBody(78));
4043         resp2.setHeader("Cache-Control","max-age=3600");
4044         resp2.setHeader("Content-Range","bytes 50-127/128");
4045         resp2.setHeader("Server","MockServer/1.0");
4046         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4047 
4048         backendExpectsAnyRequestAndReturn(resp2);
4049 
4050         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4051                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4052 
4053         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4054         resp3.setEntity(HttpTestUtils.makeBody(128));
4055         resp3.setHeader("Server","MockServer/1.0");
4056         resp3.setHeader("Date", DateUtils.formatDate(now));
4057 
4058         backendExpectsAnyRequestAndReturn(resp3);
4059 
4060         replayMocks();
4061         impl.execute(route, req1, context, null);
4062         impl.execute(route, req2, context, null);
4063         impl.execute(route, req3, context, null);
4064         verifyMocks();
4065     }
4066 
4067     @Test
4068     public void testCannotCombinePartialResponseIfCacheEntryDoesNotHaveACacheValidator()
4069         throws Exception {
4070 
4071         final Date now = new Date();
4072         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4073         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4074 
4075         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4076                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4077         req1.setHeader("Range","bytes=0-49");
4078 
4079         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4080         resp1.setEntity(HttpTestUtils.makeBody(50));
4081         resp1.setHeader("Cache-Control","max-age=3600");
4082         resp1.setHeader("Content-Range","bytes 0-49/128");
4083         resp1.setHeader("Server","MockServer/1.0");
4084         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4085 
4086         backendExpectsAnyRequestAndReturn(resp1);
4087 
4088         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4089                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4090         req2.setHeader("Range","bytes=50-127");
4091 
4092         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4093         resp2.setEntity(HttpTestUtils.makeBody(78));
4094         resp2.setHeader("Cache-Control","max-age=3600");
4095         resp2.setHeader("Content-Range","bytes 50-127/128");
4096         resp2.setHeader("ETag","\"etag1\"");
4097         resp2.setHeader("Server","MockServer/1.0");
4098         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4099 
4100         backendExpectsAnyRequestAndReturn(resp2);
4101 
4102         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4103                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4104 
4105         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4106         resp3.setEntity(HttpTestUtils.makeBody(128));
4107         resp3.setHeader("Server","MockServer/1.0");
4108         resp3.setHeader("Date", DateUtils.formatDate(now));
4109 
4110         backendExpectsAnyRequestAndReturn(resp3);
4111 
4112         replayMocks();
4113         impl.execute(route, req1, context, null);
4114         impl.execute(route, req2, context, null);
4115         impl.execute(route, req3, context, null);
4116         verifyMocks();
4117     }
4118 
4119     @Test
4120     public void testCannotCombinePartialResponseIfCacheValidatorsDoNotStronglyMatch()
4121         throws Exception {
4122 
4123         final Date now = new Date();
4124         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4125         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4126 
4127         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4128                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4129         req1.setHeader("Range","bytes=0-49");
4130 
4131         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4132         resp1.setEntity(HttpTestUtils.makeBody(50));
4133         resp1.setHeader("Cache-Control","max-age=3600");
4134         resp1.setHeader("Content-Range","bytes 0-49/128");
4135         resp1.setHeader("ETag","\"etag1\"");
4136         resp1.setHeader("Server","MockServer/1.0");
4137         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4138 
4139         backendExpectsAnyRequestAndReturn(resp1);
4140 
4141         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4142                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4143         req2.setHeader("Range","bytes=50-127");
4144 
4145         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4146         resp2.setEntity(HttpTestUtils.makeBody(78));
4147         resp2.setHeader("Cache-Control","max-age=3600");
4148         resp2.setHeader("Content-Range","bytes 50-127/128");
4149         resp2.setHeader("ETag","\"etag2\"");
4150         resp2.setHeader("Server","MockServer/1.0");
4151         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4152 
4153         backendExpectsAnyRequestAndReturn(resp2);
4154 
4155         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4156                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4157 
4158         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4159         resp3.setEntity(HttpTestUtils.makeBody(128));
4160         resp3.setHeader("Server","MockServer/1.0");
4161         resp3.setHeader("Date", DateUtils.formatDate(now));
4162 
4163         backendExpectsAnyRequestAndReturn(resp3);
4164 
4165         replayMocks();
4166         impl.execute(route, req1, context, null);
4167         impl.execute(route, req2, context, null);
4168         impl.execute(route, req3, context, null);
4169         verifyMocks();
4170     }
4171 
4172     @Test
4173     public void testMustDiscardLeastRecentPartialResponseIfIncomingRequestDoesNotHaveCacheValidator()
4174         throws Exception {
4175 
4176         final Date now = new Date();
4177         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4178         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4179 
4180         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4181                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4182         req1.setHeader("Range","bytes=0-49");
4183 
4184         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4185         resp1.setEntity(HttpTestUtils.makeBody(50));
4186         resp1.setHeader("Cache-Control","max-age=3600");
4187         resp1.setHeader("Content-Range","bytes 0-49/128");
4188         resp1.setHeader("ETag","\"etag1\"");
4189         resp1.setHeader("Server","MockServer/1.0");
4190         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4191 
4192         backendExpectsAnyRequestAndReturn(resp1);
4193 
4194         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4195                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4196         req2.setHeader("Range","bytes=50-127");
4197 
4198         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4199         resp2.setEntity(HttpTestUtils.makeBody(78));
4200         resp2.setHeader("Cache-Control","max-age=3600");
4201         resp2.setHeader("Content-Range","bytes 50-127/128");
4202         resp2.setHeader("Server","MockServer/1.0");
4203         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4204 
4205         backendExpectsAnyRequestAndReturn(resp2);
4206 
4207         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4208                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4209         req3.setHeader("Range","bytes=0-49");
4210 
4211         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4212         resp3.setEntity(HttpTestUtils.makeBody(128));
4213         resp3.setHeader("Server","MockServer/1.0");
4214         resp3.setHeader("Date", DateUtils.formatDate(now));
4215 
4216         // must make this request; cannot serve from cache
4217         backendExpectsAnyRequestAndReturn(resp3);
4218 
4219         replayMocks();
4220         impl.execute(route, req1, context, null);
4221         impl.execute(route, req2, context, null);
4222         impl.execute(route, req3, context, null);
4223         verifyMocks();
4224     }
4225 
4226     @Test
4227     public void testMustDiscardLeastRecentPartialResponseIfCachedResponseDoesNotHaveCacheValidator()
4228         throws Exception {
4229 
4230         final Date now = new Date();
4231         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4232         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4233 
4234         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4235                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4236         req1.setHeader("Range","bytes=0-49");
4237 
4238         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4239         resp1.setEntity(HttpTestUtils.makeBody(50));
4240         resp1.setHeader("Cache-Control","max-age=3600");
4241         resp1.setHeader("Content-Range","bytes 0-49/128");
4242         resp1.setHeader("Server","MockServer/1.0");
4243         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4244 
4245         backendExpectsAnyRequestAndReturn(resp1);
4246 
4247         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4248                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4249         req2.setHeader("Range","bytes=50-127");
4250 
4251         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4252         resp2.setEntity(HttpTestUtils.makeBody(78));
4253         resp2.setHeader("Cache-Control","max-age=3600");
4254         resp2.setHeader("Content-Range","bytes 50-127/128");
4255         resp2.setHeader("ETag","\"etag1\"");
4256         resp2.setHeader("Server","MockServer/1.0");
4257         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4258 
4259         backendExpectsAnyRequestAndReturn(resp2);
4260 
4261         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4262                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4263         req3.setHeader("Range","bytes=0-49");
4264 
4265         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4266         resp3.setEntity(HttpTestUtils.makeBody(128));
4267         resp3.setHeader("Server","MockServer/1.0");
4268         resp3.setHeader("Date", DateUtils.formatDate(now));
4269 
4270         // must make this request; cannot serve from cache
4271         backendExpectsAnyRequestAndReturn(resp3);
4272 
4273         replayMocks();
4274         impl.execute(route, req1, context, null);
4275         impl.execute(route, req2, context, null);
4276         impl.execute(route, req3, context, null);
4277         verifyMocks();
4278     }
4279 
4280     @Test
4281     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatch()
4282         throws Exception {
4283 
4284         final Date now = new Date();
4285         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4286         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4287 
4288         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4289                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4290         req1.setHeader("Range","bytes=0-49");
4291 
4292         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4293         resp1.setEntity(HttpTestUtils.makeBody(50));
4294         resp1.setHeader("Cache-Control","max-age=3600");
4295         resp1.setHeader("Content-Range","bytes 0-49/128");
4296         resp1.setHeader("Etag","\"etag1\"");
4297         resp1.setHeader("Server","MockServer/1.0");
4298         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4299 
4300         backendExpectsAnyRequestAndReturn(resp1);
4301 
4302         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4303                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4304         req2.setHeader("Range","bytes=50-127");
4305 
4306         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4307         resp2.setEntity(HttpTestUtils.makeBody(78));
4308         resp2.setHeader("Cache-Control","max-age=3600");
4309         resp2.setHeader("Content-Range","bytes 50-127/128");
4310         resp2.setHeader("ETag","\"etag2\"");
4311         resp2.setHeader("Server","MockServer/1.0");
4312         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4313 
4314         backendExpectsAnyRequestAndReturn(resp2);
4315 
4316         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4317                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4318         req3.setHeader("Range","bytes=0-49");
4319 
4320         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4321         resp3.setEntity(HttpTestUtils.makeBody(128));
4322         resp3.setHeader("Server","MockServer/1.0");
4323         resp3.setHeader("Date", DateUtils.formatDate(now));
4324 
4325         // must make this request; cannot serve from cache
4326         backendExpectsAnyRequestAndReturn(resp3);
4327 
4328         replayMocks();
4329         impl.execute(route, req1, context, null);
4330         impl.execute(route, req2, context, null);
4331         impl.execute(route, req3, context, null);
4332         verifyMocks();
4333     }
4334 
4335     @Test
4336     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatchEvenIfResponsesOutOfOrder()
4337         throws Exception {
4338 
4339         final Date now = new Date();
4340         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4341         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4342 
4343         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4344                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4345         req1.setHeader("Range","bytes=0-49");
4346 
4347         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4348         resp1.setEntity(HttpTestUtils.makeBody(50));
4349         resp1.setHeader("Cache-Control","max-age=3600");
4350         resp1.setHeader("Content-Range","bytes 0-49/128");
4351         resp1.setHeader("Etag","\"etag1\"");
4352         resp1.setHeader("Server","MockServer/1.0");
4353         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4354 
4355         backendExpectsAnyRequestAndReturn(resp1);
4356 
4357         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4358                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4359         req2.setHeader("Range","bytes=50-127");
4360 
4361         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4362         resp2.setEntity(HttpTestUtils.makeBody(78));
4363         resp2.setHeader("Cache-Control","max-age=3600");
4364         resp2.setHeader("Content-Range","bytes 50-127/128");
4365         resp2.setHeader("ETag","\"etag2\"");
4366         resp2.setHeader("Server","MockServer/1.0");
4367         resp2.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4368 
4369         backendExpectsAnyRequestAndReturn(resp2);
4370 
4371         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4372                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4373         req3.setHeader("Range","bytes=50-127");
4374 
4375         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4376         resp3.setEntity(HttpTestUtils.makeBody(128));
4377         resp3.setHeader("Server","MockServer/1.0");
4378         resp3.setHeader("Date", DateUtils.formatDate(now));
4379 
4380         // must make this request; cannot serve from cache
4381         backendExpectsAnyRequestAndReturn(resp3);
4382 
4383         replayMocks();
4384         impl.execute(route, req1, context, null);
4385         impl.execute(route, req2, context, null);
4386         impl.execute(route, req3, context, null);
4387         verifyMocks();
4388     }
4389 
4390     @Test
4391     public void testMustDiscardCachedPartialResponseIfCacheValidatorsDoNotStronglyMatchAndDateHeadersAreEqual()
4392         throws Exception {
4393 
4394         final Date now = new Date();
4395         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4396 
4397         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4398                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4399         req1.setHeader("Range","bytes=0-49");
4400 
4401         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4402         resp1.setEntity(HttpTestUtils.makeBody(50));
4403         resp1.setHeader("Cache-Control","max-age=3600");
4404         resp1.setHeader("Content-Range","bytes 0-49/128");
4405         resp1.setHeader("Etag","\"etag1\"");
4406         resp1.setHeader("Server","MockServer/1.0");
4407         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4408 
4409         backendExpectsAnyRequestAndReturn(resp1);
4410 
4411         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4412                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4413         req2.setHeader("Range","bytes=50-127");
4414 
4415         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4416         resp2.setEntity(HttpTestUtils.makeBody(78));
4417         resp2.setHeader("Cache-Control","max-age=3600");
4418         resp2.setHeader("Content-Range","bytes 50-127/128");
4419         resp2.setHeader("ETag","\"etag2\"");
4420         resp2.setHeader("Server","MockServer/1.0");
4421         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4422 
4423         backendExpectsAnyRequestAndReturn(resp2);
4424 
4425         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4426                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4427         req3.setHeader("Range","bytes=0-49");
4428 
4429         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4430         resp3.setEntity(HttpTestUtils.makeBody(128));
4431         resp3.setHeader("Server","MockServer/1.0");
4432         resp3.setHeader("Date", DateUtils.formatDate(now));
4433 
4434         // must make this request; cannot serve from cache
4435         backendExpectsAnyRequestAndReturn(resp3);
4436 
4437         replayMocks();
4438         impl.execute(route, req1, context, null);
4439         impl.execute(route, req2, context, null);
4440         impl.execute(route, req3, context, null);
4441         verifyMocks();
4442     }
4443 
4444     /* "When the cache receives a subsequent request whose Request-URI
4445      * specifies one or more cache entries including a Vary header
4446      * field, the cache MUST NOT use such a cache entry to construct a
4447      * response to the new request unless all of the selecting
4448      * request-headers present in the new request match the
4449      * corresponding stored request-headers in the original request."
4450      *
4451      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4452      */
4453     @Test
4454     public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch()
4455         throws Exception {
4456 
4457         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4458                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4459         req1.setHeader("Accept-Encoding","gzip");
4460 
4461         final HttpResponse resp1 = HttpTestUtils.make200Response();
4462         resp1.setHeader("ETag","\"etag1\"");
4463         resp1.setHeader("Cache-Control","max-age=3600");
4464         resp1.setHeader("Vary","Accept-Encoding");
4465 
4466         backendExpectsAnyRequestAndReturn(resp1);
4467 
4468         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4469                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4470         req2.removeHeaders("Accept-Encoding");
4471 
4472         final HttpResponse resp2 = HttpTestUtils.make200Response();
4473         resp2.setHeader("ETag","\"etag1\"");
4474         resp2.setHeader("Cache-Control","max-age=3600");
4475 
4476         // not allowed to have a cache hit; must forward request
4477         backendExpectsAnyRequestAndReturn(resp2);
4478 
4479         replayMocks();
4480         impl.execute(route, req1, context, null);
4481         impl.execute(route, req2, context, null);
4482         verifyMocks();
4483     }
4484 
4485     /* "A Vary header field-value of "*" always fails to match and
4486      * subsequent requests on that resource can only be properly
4487      * interpreted by the origin server."
4488      *
4489      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4490      */
4491     @Test
4492     public void testCannotServeFromCacheForVaryStar() throws Exception {
4493         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4494                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4495 
4496         final HttpResponse resp1 = HttpTestUtils.make200Response();
4497         resp1.setHeader("ETag","\"etag1\"");
4498         resp1.setHeader("Cache-Control","max-age=3600");
4499         resp1.setHeader("Vary","*");
4500 
4501         backendExpectsAnyRequestAndReturn(resp1);
4502 
4503         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4504                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4505 
4506         final HttpResponse resp2 = HttpTestUtils.make200Response();
4507         resp2.setHeader("ETag","\"etag1\"");
4508         resp2.setHeader("Cache-Control","max-age=3600");
4509 
4510         // not allowed to have a cache hit; must forward request
4511         backendExpectsAnyRequestAndReturn(resp2);
4512 
4513         replayMocks();
4514         impl.execute(route, req1, context, null);
4515         impl.execute(route, req2, context, null);
4516         verifyMocks();
4517     }
4518 
4519     /* " If the selecting request header fields for the cached entry
4520      * do not match the selecting request header fields of the new
4521      * request, then the cache MUST NOT use a cached entry to satisfy
4522      * the request unless it first relays the new request to the
4523      * origin server in a conditional request and the server responds
4524      * with 304 (Not Modified), including an entity tag or
4525      * Content-Location that indicates the entity to be used.
4526      *
4527      * If an entity tag was assigned to a cached representation, the
4528      * forwarded request SHOULD be conditional and include the entity
4529      * tags in an If-None-Match header field from all its cache
4530      * entries for the resource. This conveys to the server the set of
4531      * entities currently held by the cache, so that if any one of
4532      * these entities matches the requested entity, the server can use
4533      * the ETag header field in its 304 (Not Modified) response to
4534      * tell the cache which entry is appropriate. If the entity-tag of
4535      * the new response matches that of an existing entry, the new
4536      * response SHOULD be used to update the header fields of the
4537      * existing entry, and the result MUST be returned to the client.
4538      *
4539      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4540      */
4541     @Test
4542     public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated()
4543         throws Exception {
4544 
4545         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4546                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4547         req1.setHeader("User-Agent","MyBrowser/1.0");
4548 
4549         final HttpResponse resp1 = HttpTestUtils.make200Response();
4550         resp1.setHeader("ETag","\"etag1\"");
4551         resp1.setHeader("Cache-Control","max-age=3600");
4552         resp1.setHeader("Vary","User-Agent");
4553         resp1.setHeader("Content-Type","application/octet-stream");
4554 
4555         backendExpectsAnyRequestAndReturn(resp1);
4556 
4557         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4558                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4559         req2.setHeader("User-Agent","MyBrowser/1.5");
4560 
4561         final HttpRequestWrapper conditional = HttpRequestWrapper.wrap(
4562                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4563         conditional.setHeader("User-Agent","MyBrowser/1.5");
4564         conditional.setHeader("If-None-Match","\"etag1\"");
4565 
4566         final HttpResponse resp200 = HttpTestUtils.make200Response();
4567         resp200.setHeader("ETag","\"etag1\"");
4568         resp200.setHeader("Vary","User-Agent");
4569 
4570         final HttpResponse resp304 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
4571         resp304.setHeader("ETag","\"etag1\"");
4572         resp304.setHeader("Vary","User-Agent");
4573 
4574         final Capture<HttpRequestWrapper> condCap = new Capture<HttpRequestWrapper>();
4575         final Capture<HttpRequestWrapper> uncondCap = new Capture<HttpRequestWrapper>();
4576 
4577         EasyMock.expect(
4578                 mockBackend.execute(
4579                         EasyMock.isA(HttpRoute.class),
4580                         EasyMock.and(eqRequest(conditional), EasyMock.capture(condCap)),
4581                         EasyMock.isA(HttpClientContext.class),
4582                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4583                                 Proxies.enhanceResponse(resp304)).times(0,1);
4584         EasyMock.expect(
4585                 mockBackend.execute(
4586                         EasyMock.isA(HttpRoute.class),
4587                         EasyMock.and(eqRequest(req2), EasyMock.capture(uncondCap)),
4588                         EasyMock.isA(HttpClientContext.class),
4589                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4590                                 Proxies.enhanceResponse(resp200)).times(0,1);
4591 
4592         replayMocks();
4593         impl.execute(route, req1, context, null);
4594         final HttpResponse result = impl.execute(route, req2, context, null);
4595         verifyMocks();
4596 
4597         if (HttpStatus.SC_OK == result.getStatusLine().getStatusCode()) {
4598             Assert.assertTrue(condCap.hasCaptured()
4599                               || uncondCap.hasCaptured());
4600             if (uncondCap.hasCaptured()) {
4601                 Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
4602             }
4603         }
4604     }
4605 
4606     /* "A cache that receives an incomplete response (for example,
4607      * with fewer bytes of data than specified in a Content-Length
4608      * header) MAY store the response. However, the cache MUST treat
4609      * this as a partial response. Partial responses MAY be combined
4610      * as described in section 13.5.4; the result might be a full
4611      * response or might still be partial. A cache MUST NOT return a
4612      * partial response to a client without explicitly marking it as
4613      * such, using the 206 (Partial Content) status code. A cache MUST
4614      * NOT return a partial response using a status code of 200 (OK)."
4615      *
4616      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.8
4617      */
4618     @Test
4619     public void testIncompleteResponseMustNotBeReturnedToClientWithoutMarkingItAs206() throws Exception {
4620         originResponse.setEntity(HttpTestUtils.makeBody(128));
4621         originResponse.setHeader("Content-Length","256");
4622 
4623         backendExpectsAnyRequest().andReturn(originResponse);
4624 
4625         replayMocks();
4626         final HttpResponse result = impl.execute(route, request, context, null);
4627         verifyMocks();
4628 
4629         final int status = result.getStatusLine().getStatusCode();
4630         Assert.assertFalse(HttpStatus.SC_OK == status);
4631         if (status > 200 && status <= 299
4632             && HttpTestUtils.equivalent(originResponse.getEntity(),
4633                                         result.getEntity())) {
4634             Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT == status);
4635         }
4636     }
4637 
4638     /* "Some HTTP methods MUST cause a cache to invalidate an
4639      * entity. This is either the entity referred to by the
4640      * Request-URI, or by the Location or Content-Location headers (if
4641      * present). These methods are:
4642      * - PUT
4643      * - DELETE
4644      * - POST
4645      *
4646      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
4647      */
4648     protected void testUnsafeOperationInvalidatesCacheForThatUri(
4649             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4650         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4651                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4652         final HttpResponse resp1 = HttpTestUtils.make200Response();
4653         resp1.setHeader("Cache-Control","public, max-age=3600");
4654 
4655         backendExpectsAnyRequestAndReturn(resp1);
4656 
4657         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4658 
4659         backendExpectsAnyRequestAndReturn(resp2);
4660 
4661         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4662                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4663         final HttpResponse resp3 = HttpTestUtils.make200Response();
4664         resp3.setHeader("Cache-Control","public, max-age=3600");
4665 
4666         // this origin request MUST happen due to invalidation
4667         backendExpectsAnyRequestAndReturn(resp3);
4668 
4669         replayMocks();
4670         impl.execute(route, req1, context, null);
4671         impl.execute(route, unsafeReq, context, null);
4672         impl.execute(route, req3, context, null);
4673         verifyMocks();
4674     }
4675 
4676     @Test
4677     public void testPutToUriInvalidatesCacheForThatUri() throws Exception {
4678         final HttpRequest req = makeRequestWithBody("PUT","/");
4679         testUnsafeOperationInvalidatesCacheForThatUri(HttpRequestWrapper.wrap(req));
4680     }
4681 
4682     @Test
4683     public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
4684         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE","/"));
4685         testUnsafeOperationInvalidatesCacheForThatUri(req);
4686     }
4687 
4688     @Test
4689     public void testPostToUriInvalidatesCacheForThatUri() throws Exception {
4690         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4691         testUnsafeOperationInvalidatesCacheForThatUri(req);
4692     }
4693 
4694     protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
4695             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4696         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4697                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4698         final HttpResponse resp1 = HttpTestUtils.make200Response();
4699         resp1.setHeader("Cache-Control","public, max-age=3600");
4700 
4701         backendExpectsAnyRequestAndReturn(resp1);
4702 
4703         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4704 
4705         backendExpectsAnyRequestAndReturn(resp2);
4706 
4707         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4708                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4709         final HttpResponse resp3 = HttpTestUtils.make200Response();
4710         resp3.setHeader("Cache-Control","public, max-age=3600");
4711 
4712         // this origin request MUST happen due to invalidation
4713         backendExpectsAnyRequestAndReturn(resp3);
4714 
4715         replayMocks();
4716         impl.execute(route, req1, context, null);
4717         impl.execute(route, unsafeReq, context, null);
4718         impl.execute(route, req3, context, null);
4719         verifyMocks();
4720     }
4721 
4722     protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
4723             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4724         unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
4725         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4726     }
4727 
4728     protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
4729             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4730         unsafeReq.setHeader("Content-Location","/content");
4731         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4732     }
4733 
4734     protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
4735             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4736         unsafeReq.setHeader("Location","http://foo.example.com/content");
4737         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4738     }
4739 
4740     @Test
4741     public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4742         final HttpRequestWrapper req2 = makeRequestWithBody("PUT","/");
4743         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
4744     }
4745 
4746     @Test
4747     public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4748         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4749         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4750     }
4751 
4752     @Test
4753     public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4754         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4755         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4756     }
4757 
4758     @Test
4759     public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4760         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4761         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4762     }
4763 
4764     @Test
4765     public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4766         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4767         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4768     }
4769 
4770     @Test
4771     public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4772         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4773         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4774     }
4775 
4776     @Test
4777     public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4778         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4779         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4780     }
4781 
4782     @Test
4783     public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4784         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4785         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4786     }
4787 
4788     @Test
4789     public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
4790         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4791         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4792     }
4793 
4794     /* "In order to prevent denial of service attacks, an invalidation based on the URI
4795      *  in a Location or Content-Location header MUST only be performed if the host part
4796      *  is the same as in the Request-URI."
4797      *
4798      *  http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
4799      */
4800     protected void testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(
4801             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4802 
4803         final HttpHost otherHost = new HttpHost("bar.example.com", 80);
4804         final HttpRoute otherRoute = new HttpRoute(otherHost);
4805         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4806                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4807         final HttpResponse resp1 = HttpTestUtils.make200Response();
4808         resp1.setHeader("Cache-Control","public, max-age=3600");
4809 
4810         backendExpectsAnyRequestAndReturn(resp1);
4811 
4812         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4813 
4814         backendExpectsAnyRequestAndReturn(resp2);
4815 
4816         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4817                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4818 
4819         replayMocks();
4820         impl.execute(otherRoute, req1, context, null);
4821         impl.execute(route, unsafeReq, context, null);
4822         impl.execute(otherRoute, req3, context, null);
4823         verifyMocks();
4824     }
4825 
4826     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(
4827             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4828         unsafeReq.setHeader("Content-Location","http://bar.example.com/content");
4829         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4830     }
4831 
4832     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(
4833             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4834         unsafeReq.setHeader("Location","http://bar.example.com/content");
4835         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4836     }
4837 
4838     protected HttpRequestWrapper makeRequestWithBody(final String method, final String requestUri) {
4839         final HttpEntityEnclosingRequest req =
4840             new BasicHttpEntityEnclosingRequest(method, requestUri, HttpVersion.HTTP_1_1);
4841         final int nbytes = 128;
4842         req.setEntity(HttpTestUtils.makeBody(nbytes));
4843         req.setHeader("Content-Length",""+nbytes);
4844         return HttpRequestWrapper.wrap(req);
4845     }
4846 
4847     @Test
4848     public void testPutDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4849         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4850         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4851     }
4852 
4853     @Test
4854     public void testPutDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4855         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4856         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4857     }
4858 
4859     @Test
4860     public void testPostDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4861         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4862         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4863     }
4864 
4865     @Test
4866     public void testPostDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4867         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4868         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4869     }
4870 
4871     @Test
4872     public void testDeleteDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4873         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
4874                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4875         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4876     }
4877 
4878     @Test
4879     public void testDeleteDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4880         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
4881                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4882         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4883     }
4884 
4885     /* "All methods that might be expected to cause modifications to the origin
4886      * server's resources MUST be written through to the origin server. This
4887      * currently includes all methods except for GET and HEAD. A cache MUST NOT
4888      * reply to such a request from a client before having transmitted the
4889      * request to the inbound server, and having received a corresponding
4890      * response from the inbound server."
4891      *
4892      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.11
4893      */
4894     private void testRequestIsWrittenThroughToOrigin(final HttpRequest req)
4895         throws Exception {
4896         final HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4897         final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(req);
4898         EasyMock.expect(
4899                 mockBackend.execute(
4900                         EasyMock.eq(route),
4901                         eqRequest(wrapper),
4902                         EasyMock.isA(HttpClientContext.class),
4903                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4904                                 Proxies.enhanceResponse(resp));
4905 
4906         replayMocks();
4907         impl.execute(route, wrapper, context, null);
4908         verifyMocks();
4909     }
4910 
4911     @Test @Ignore
4912     public void testOPTIONSRequestsAreWrittenThroughToOrigin()
4913         throws Exception {
4914         final HttpRequest req = HttpRequestWrapper.wrap(
4915                 new BasicHttpRequest("OPTIONS","*",HttpVersion.HTTP_1_1));
4916         testRequestIsWrittenThroughToOrigin(req);
4917     }
4918 
4919     @Test
4920     public void testPOSTRequestsAreWrittenThroughToOrigin()
4921         throws Exception {
4922         final HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
4923         req.setEntity(HttpTestUtils.makeBody(128));
4924         req.setHeader("Content-Length","128");
4925         testRequestIsWrittenThroughToOrigin(req);
4926     }
4927 
4928     @Test
4929     public void testPUTRequestsAreWrittenThroughToOrigin()
4930         throws Exception {
4931         final HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("PUT","/",HttpVersion.HTTP_1_1);
4932         req.setEntity(HttpTestUtils.makeBody(128));
4933         req.setHeader("Content-Length","128");
4934         testRequestIsWrittenThroughToOrigin(req);
4935     }
4936 
4937     @Test
4938     public void testDELETERequestsAreWrittenThroughToOrigin()
4939         throws Exception {
4940         final HttpRequest req = HttpRequestWrapper.wrap(
4941                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4942         testRequestIsWrittenThroughToOrigin(req);
4943     }
4944 
4945     @Test
4946     public void testTRACERequestsAreWrittenThroughToOrigin()
4947         throws Exception {
4948         final HttpRequest req = HttpRequestWrapper.wrap(
4949                 new BasicHttpRequest("TRACE","/",HttpVersion.HTTP_1_1));
4950         testRequestIsWrittenThroughToOrigin(req);
4951     }
4952 
4953     @Test
4954     public void testCONNECTRequestsAreWrittenThroughToOrigin()
4955         throws Exception {
4956         final HttpRequest req = HttpRequestWrapper.wrap(
4957                 new BasicHttpRequest("CONNECT","/",HttpVersion.HTTP_1_1));
4958         testRequestIsWrittenThroughToOrigin(req);
4959     }
4960 
4961     @Test
4962     public void testUnknownMethodRequestsAreWrittenThroughToOrigin()
4963         throws Exception {
4964         final HttpRequest req = HttpRequestWrapper.wrap(
4965                 new BasicHttpRequest("UNKNOWN","/",HttpVersion.HTTP_1_1));
4966         testRequestIsWrittenThroughToOrigin(req);
4967     }
4968 
4969     /* "If a cache receives a value larger than the largest positive
4970      * integer it can represent, or if any of its age calculations
4971      * overflows, it MUST transmit an Age header with a value of
4972      * 2147483648 (2^31)."
4973      *
4974      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6
4975      */
4976     @Test
4977     public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig()
4978         throws Exception {
4979         final String reallyOldAge = "1" + Long.MAX_VALUE;
4980         originResponse.setHeader("Age",reallyOldAge);
4981 
4982         backendExpectsAnyRequest().andReturn(originResponse);
4983 
4984         replayMocks();
4985         final HttpResponse result = impl.execute(route, request, context, null);
4986         verifyMocks();
4987 
4988         Assert.assertEquals("2147483648",
4989                             result.getFirstHeader("Age").getValue());
4990     }
4991 
4992     /* "A proxy MUST NOT modify the Allow header field even if it does not
4993      * understand all the methods specified, since the user agent might
4994      * have other means of communicating with the origin server.
4995      *
4996      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
4997      */
4998     @Test
4999     public void testDoesNotModifyAllowHeaderWithUnknownMethods()
5000         throws Exception {
5001         final String allowHeaderValue = "GET, HEAD, FOOBAR";
5002         originResponse.setHeader("Allow",allowHeaderValue);
5003         backendExpectsAnyRequest().andReturn(originResponse);
5004         replayMocks();
5005         final HttpResponse result = impl.execute(route, request, context, null);
5006         verifyMocks();
5007         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
5008                             HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
5009     }
5010 
5011     /* "When a shared cache (see section 13.7) receives a request
5012      * containing an Authorization field, it MUST NOT return the
5013      * corresponding response as a reply to any other request, unless one
5014      * of the following specific exceptions holds:
5015      *
5016      * 1. If the response includes the "s-maxage" cache-control
5017      *    directive, the cache MAY use that response in replying to a
5018      *    subsequent request. But (if the specified maximum age has
5019      *    passed) a proxy cache MUST first revalidate it with the origin
5020      *    server, using the request-headers from the new request to allow
5021      *    the origin server to authenticate the new request. (This is the
5022      *    defined behavior for s-maxage.) If the response includes "s-
5023      *    maxage=0", the proxy MUST always revalidate it before re-using
5024      *    it.
5025      *
5026      * 2. If the response includes the "must-revalidate" cache-control
5027      *    directive, the cache MAY use that response in replying to a
5028      *    subsequent request. But if the response is stale, all caches
5029      *    MUST first revalidate it with the origin server, using the
5030      *    request-headers from the new request to allow the origin server
5031      *    to authenticate the new request.
5032      *
5033      * 3. If the response includes the "public" cache-control directive,
5034      *    it MAY be returned in reply to any subsequent request.
5035      */
5036     protected void testSharedCacheRevalidatesAuthorizedResponse(
5037             final HttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception,
5038             IOException {
5039         if (config.isSharedCache()) {
5040             final String authorization = "Basic dXNlcjpwYXNzd2Q=";
5041             final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5042                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5043             req1.setHeader("Authorization",authorization);
5044 
5045             backendExpectsAnyRequestAndReturn(authorizedResponse);
5046 
5047             final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5048                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5049             final HttpResponse resp2 = HttpTestUtils.make200Response();
5050             resp2.setHeader("Cache-Control","max-age=3600");
5051 
5052             if (maxTimes > 0) {
5053                 // this request MUST happen
5054                 backendExpectsAnyRequest().andReturn(
5055                         Proxies.enhanceResponse(resp2)).times(minTimes,maxTimes);
5056             }
5057 
5058             replayMocks();
5059             impl.execute(route, req1, context, null);
5060             impl.execute(route, req2, context, null);
5061             verifyMocks();
5062         }
5063     }
5064 
5065     @Test
5066     public void testSharedCacheMustNotNormallyCacheAuthorizedResponses()
5067         throws Exception {
5068         final HttpResponse resp = HttpTestUtils.make200Response();
5069         resp.setHeader("Cache-Control","max-age=3600");
5070         resp.setHeader("ETag","\"etag\"");
5071         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
5072     }
5073 
5074     @Test
5075     public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader()
5076         throws Exception {
5077         final HttpResponse resp = HttpTestUtils.make200Response();
5078         resp.setHeader("Cache-Control","s-maxage=3600");
5079         resp.setHeader("ETag","\"etag\"");
5080         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5081     }
5082 
5083     @Test
5084     public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero()
5085         throws Exception {
5086         final HttpResponse resp = HttpTestUtils.make200Response();
5087         resp.setHeader("Cache-Control","s-maxage=0");
5088         resp.setHeader("ETag","\"etag\"");
5089         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
5090     }
5091 
5092     @Test
5093     public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate()
5094         throws Exception {
5095         final HttpResponse resp = HttpTestUtils.make200Response();
5096         resp.setHeader("Cache-Control","must-revalidate");
5097         resp.setHeader("ETag","\"etag\"");
5098         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5099     }
5100 
5101     @Test
5102     public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic()
5103         throws Exception {
5104         final HttpResponse resp = HttpTestUtils.make200Response();
5105         resp.setHeader("Cache-Control","public");
5106         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5107     }
5108 
5109     protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
5110             final HttpResponse authorizedResponse) throws Exception, IOException,
5111             ClientProtocolException {
5112         if (config.isSharedCache()) {
5113             final String authorization1 = "Basic dXNlcjpwYXNzd2Q=";
5114             final String authorization2 = "Basic dXNlcjpwYXNzd2Qy";
5115 
5116             final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5117                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5118             req1.setHeader("Authorization",authorization1);
5119 
5120             backendExpectsAnyRequestAndReturn(authorizedResponse);
5121 
5122             final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5123                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5124             req2.setHeader("Authorization",authorization2);
5125 
5126             final HttpResponse resp2 = HttpTestUtils.make200Response();
5127 
5128             final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5129             EasyMock.expect(
5130                     mockBackend.execute(
5131                             EasyMock.eq(route),
5132                             EasyMock.capture(cap),
5133                             EasyMock.isA(HttpClientContext.class),
5134                             EasyMock.<HttpExecutionAware>isNull())).andReturn(
5135                                     Proxies.enhanceResponse(resp2));
5136 
5137             replayMocks();
5138             impl.execute(route, req1, context, null);
5139             impl.execute(route, req2, context, null);
5140             verifyMocks();
5141 
5142             final HttpRequest captured = cap.getValue();
5143             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
5144                     HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
5145         }
5146     }
5147 
5148     @Test
5149     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge()
5150     throws Exception {
5151         final Date now = new Date();
5152         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5153         final HttpResponse resp1 = HttpTestUtils.make200Response();
5154         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5155         resp1.setHeader("ETag","\"etag\"");
5156         resp1.setHeader("Cache-Control","s-maxage=5");
5157 
5158         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
5159     }
5160 
5161     @Test
5162     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate()
5163     throws Exception {
5164         final Date now = new Date();
5165         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5166         final HttpResponse resp1 = HttpTestUtils.make200Response();
5167         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5168         resp1.setHeader("ETag","\"etag\"");
5169         resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
5170 
5171         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
5172     }
5173 
5174     /* "If a cache returns a stale response, either because of a max-stale
5175      * directive on a request, or because the cache is configured to
5176      * override the expiration time of a response, the cache MUST attach a
5177      * Warning header to the stale response, using Warning 110 (Response
5178      * is stale).
5179      *
5180      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
5181      *
5182      * "110 Response is stale MUST be included whenever the returned
5183      * response is stale."
5184      *
5185      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5186      */
5187     @Test
5188     public void testWarning110IsAddedToStaleResponses()
5189         throws Exception {
5190         final Date now = new Date();
5191         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5192         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5193                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5194         final HttpResponse resp1 = HttpTestUtils.make200Response();
5195         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5196         resp1.setHeader("Cache-Control","max-age=5");
5197         resp1.setHeader("Etag","\"etag\"");
5198 
5199         backendExpectsAnyRequestAndReturn(resp1);
5200 
5201         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5202                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5203         req2.setHeader("Cache-Control","max-stale=60");
5204         final HttpResponse resp2 = HttpTestUtils.make200Response();
5205 
5206         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5207         EasyMock.expect(
5208                 mockBackend.execute(
5209                         EasyMock.eq(route),
5210                         EasyMock.capture(cap),
5211                         EasyMock.isA(HttpClientContext.class),
5212                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5213                                 Proxies.enhanceResponse(resp2)).times(0,1);
5214 
5215         replayMocks();
5216         impl.execute(route, req1, context, null);
5217         final HttpResponse result = impl.execute(route, req2, context, null);
5218         verifyMocks();
5219 
5220         if (!cap.hasCaptured()) {
5221             boolean found110Warning = false;
5222             for(final Header h : result.getHeaders("Warning")) {
5223                 for(final HeaderElement elt : h.getElements()) {
5224                     final String[] parts = elt.getName().split("\\s");
5225                     if ("110".equals(parts[0])) {
5226                         found110Warning = true;
5227                         break;
5228                     }
5229                 }
5230             }
5231             Assert.assertTrue(found110Warning);
5232         }
5233     }
5234 
5235     /* "Field names MUST NOT be included with the no-cache directive in a
5236      * request."
5237      *
5238      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5239      */
5240     @Test
5241     public void testDoesNotTransmitNoCacheDirectivesWithFieldsDownstream()
5242         throws Exception {
5243         request.setHeader("Cache-Control","no-cache=\"X-Field\"");
5244         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5245         EasyMock.expect(mockBackend.execute(
5246                 EasyMock.eq(route),
5247                 EasyMock.capture(cap),
5248                 EasyMock.isA(HttpClientContext.class),
5249                 EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(0,1);
5250 
5251         replayMocks();
5252         try {
5253             impl.execute(route, request, context, null);
5254         } catch (final ClientProtocolException acceptable) {
5255         }
5256         verifyMocks();
5257 
5258         if (cap.hasCaptured()) {
5259             final HttpRequest captured = cap.getValue();
5260             for(final Header h : captured.getHeaders("Cache-Control")) {
5261                 for(final HeaderElement elt : h.getElements()) {
5262                     if ("no-cache".equals(elt.getName())) {
5263                         Assert.assertNull(elt.getValue());
5264                     }
5265                 }
5266             }
5267         }
5268     }
5269 
5270     /* "The request includes a "no-cache" cache-control directive or, for
5271      * compatibility with HTTP/1.0 clients, "Pragma: no-cache".... The
5272      * server MUST NOT use a cached copy when responding to such a request."
5273      *
5274      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5275      */
5276     protected void testCacheIsNotUsedWhenRespondingToRequest(final HttpRequestWrapper req)
5277         throws Exception {
5278         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5279                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5280         final HttpResponse resp1 = HttpTestUtils.make200Response();
5281         resp1.setHeader("Etag","\"etag\"");
5282         resp1.setHeader("Cache-Control","max-age=3600");
5283 
5284         backendExpectsAnyRequestAndReturn(resp1);
5285 
5286         final HttpResponse resp2 = HttpTestUtils.make200Response();
5287         resp2.setHeader("Etag","\"etag2\"");
5288         resp2.setHeader("Cache-Control","max-age=1200");
5289 
5290         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5291         EasyMock.expect(mockBackend.execute(
5292                 EasyMock.eq(route),
5293                 EasyMock.capture(cap),
5294                 EasyMock.isA(HttpClientContext.class),
5295                 EasyMock.<HttpExecutionAware>isNull())).andReturn(
5296                         Proxies.enhanceResponse(resp2));
5297 
5298         replayMocks();
5299         impl.execute(route, req1, context, null);
5300         final HttpResponse result = impl.execute(route, req, context, null);
5301         verifyMocks();
5302 
5303         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
5304         final HttpRequest captured = cap.getValue();
5305         Assert.assertTrue(HttpTestUtils.equivalent(req, captured));
5306     }
5307 
5308     @Test
5309     public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache()
5310         throws Exception {
5311         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
5312                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5313         req.setHeader("Cache-Control","no-cache");
5314         testCacheIsNotUsedWhenRespondingToRequest(req);
5315     }
5316 
5317     @Test
5318     public void testCacheIsNotUsedWhenRespondingToRequestWithPragmaNoCache()
5319         throws Exception {
5320         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
5321                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5322         req.setHeader("Pragma","no-cache");
5323         testCacheIsNotUsedWhenRespondingToRequest(req);
5324     }
5325 
5326     /* "When the must-revalidate directive is present in a response received
5327      * by a cache, that cache MUST NOT use the entry after it becomes stale
5328      * to respond to a subsequent request without first revalidating it with
5329      * the origin server. (I.e., the cache MUST do an end-to-end
5330      * revalidation every time, if, based solely on the origin server's
5331      * Expires or max-age value, the cached response is stale.)"
5332      *
5333      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5334      */
5335     protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
5336             final HttpResponse staleResponse) throws Exception {
5337         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5338                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5339 
5340         backendExpectsAnyRequestAndReturn(staleResponse);
5341 
5342         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5343                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5344         req2.setHeader("Cache-Control","max-stale=3600");
5345         final HttpResponse resp2 = HttpTestUtils.make200Response();
5346         resp2.setHeader("ETag","\"etag2\"");
5347         resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
5348 
5349         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5350         // this request MUST happen
5351         EasyMock.expect(
5352                 mockBackend.execute(
5353                         EasyMock.eq(route),
5354                         EasyMock.capture(cap),
5355                         EasyMock.isA(HttpClientContext.class),
5356                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5357                                 Proxies.enhanceResponse(resp2));
5358 
5359         replayMocks();
5360         impl.execute(route, req1, context, null);
5361         impl.execute(route, req2, context, null);
5362         verifyMocks();
5363 
5364         final HttpRequest reval = cap.getValue();
5365         boolean foundMaxAge0 = false;
5366         for(final Header h : reval.getHeaders("Cache-Control")) {
5367             for(final HeaderElement elt : h.getElements()) {
5368                 if ("max-age".equalsIgnoreCase(elt.getName())
5369                     && "0".equals(elt.getValue())) {
5370                     foundMaxAge0 = true;
5371                 }
5372             }
5373         }
5374         Assert.assertTrue(foundMaxAge0);
5375     }
5376 
5377     @Test
5378     public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin()
5379         throws Exception {
5380         final HttpResponse response = HttpTestUtils.make200Response();
5381         final Date now = new Date();
5382         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5383         response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5384         response.setHeader("ETag","\"etag1\"");
5385         response.setHeader("Cache-Control","max-age=5, must-revalidate");
5386 
5387         testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
5388     }
5389 
5390 
5391     /* "In all circumstances an HTTP/1.1 cache MUST obey the must-revalidate
5392      * directive; in particular, if the cache cannot reach the origin server
5393      * for any reason, it MUST generate a 504 (Gateway Timeout) response."
5394      */
5395     protected void testGenerates504IfCannotRevalidateStaleResponse(
5396             final HttpResponse staleResponse) throws Exception {
5397         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5398                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5399 
5400         backendExpectsAnyRequestAndReturn(staleResponse);
5401 
5402         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5403                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5404 
5405         backendExpectsAnyRequest().andThrow(new SocketTimeoutException());
5406 
5407         replayMocks();
5408         impl.execute(route, req1, context, null);
5409         final HttpResponse result = impl.execute(route, req2, context, null);
5410         verifyMocks();
5411 
5412         Assert.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
5413                             result.getStatusLine().getStatusCode());
5414     }
5415 
5416     @Test
5417     public void testGenerates504IfCannotRevalidateAMustRevalidateEntry()
5418         throws Exception {
5419         final HttpResponse resp1 = HttpTestUtils.make200Response();
5420         final Date now = new Date();
5421         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5422         resp1.setHeader("ETag","\"etag\"");
5423         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5424         resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
5425 
5426         testGenerates504IfCannotRevalidateStaleResponse(resp1);
5427     }
5428 
5429     /* "The proxy-revalidate directive has the same meaning as the must-
5430      * revalidate directive, except that it does not apply to non-shared
5431      * user agent caches."
5432      *
5433      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5434      */
5435     @Test
5436     public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin()
5437         throws Exception {
5438         if (config.isSharedCache()) {
5439             final HttpResponse response = HttpTestUtils.make200Response();
5440             final Date now = new Date();
5441             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5442             response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5443             response.setHeader("ETag","\"etag1\"");
5444             response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
5445 
5446             testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
5447         }
5448     }
5449 
5450     @Test
5451     public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry()
5452         throws Exception {
5453         if (config.isSharedCache()) {
5454             final HttpResponse resp1 = HttpTestUtils.make200Response();
5455             final Date now = new Date();
5456             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5457             resp1.setHeader("ETag","\"etag\"");
5458             resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5459             resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
5460 
5461             testGenerates504IfCannotRevalidateStaleResponse(resp1);
5462         }
5463     }
5464 
5465     /* "[The cache control directive] "private" Indicates that all or part of
5466      * the response message is intended for a single user and MUST NOT be
5467      * cached by a shared cache."
5468      *
5469      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
5470      */
5471     @Test
5472     public void testCacheControlPrivateIsNotCacheableBySharedCache()
5473     throws Exception {
5474        if (config.isSharedCache()) {
5475                final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5476                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5477                final HttpResponse resp1 = HttpTestUtils.make200Response();
5478                resp1.setHeader("Cache-Control","private,max-age=3600");
5479 
5480                backendExpectsAnyRequestAndReturn(resp1);
5481 
5482                final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5483                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5484                final HttpResponse resp2 = HttpTestUtils.make200Response();
5485                // this backend request MUST happen
5486                backendExpectsAnyRequestAndReturn(resp2);
5487 
5488                replayMocks();
5489                impl.execute(route, req1, context, null);
5490                impl.execute(route, req2, context, null);
5491                verifyMocks();
5492        }
5493     }
5494 
5495     @Test
5496     public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache()
5497     throws Exception {
5498        if (config.isSharedCache()) {
5499                final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5500                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5501                final HttpResponse resp1 = HttpTestUtils.make200Response();
5502                resp1.setHeader("X-Personal","stuff");
5503                resp1.setHeader("Cache-Control","private=\"X-Personal\",s-maxage=3600");
5504 
5505                backendExpectsAnyRequestAndReturn(resp1);
5506 
5507                final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5508                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5509                final HttpResponse resp2 = HttpTestUtils.make200Response();
5510 
5511                // this backend request MAY happen
5512                backendExpectsAnyRequestAndReturn(resp2).times(0,1);
5513 
5514                replayMocks();
5515                impl.execute(route, req1, context, null);
5516                final HttpResponse result = impl.execute(route, req2, context, null);
5517                verifyMocks();
5518                Assert.assertNull(result.getFirstHeader("X-Personal"));
5519        }
5520     }
5521 
5522     /* "If the no-cache directive does not specify a field-name, then a
5523      * cache MUST NOT use the response to satisfy a subsequent request
5524      * without successful revalidation with the origin server. This allows
5525      * an origin server to prevent caching even by caches that have been
5526      * configured to return stale responses to client requests."
5527      *
5528      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
5529      */
5530     @Test
5531     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation()
5532     throws Exception {
5533         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5534                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5535         final HttpResponse resp1 = HttpTestUtils.make200Response();
5536         resp1.setHeader("ETag","\"etag\"");
5537         resp1.setHeader("Cache-Control","no-cache");
5538 
5539         backendExpectsAnyRequestAndReturn(resp1);
5540 
5541         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5542                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5543         final HttpResponse resp2 = HttpTestUtils.make200Response();
5544 
5545         // this MUST happen
5546         backendExpectsAnyRequestAndReturn(resp2);
5547 
5548         replayMocks();
5549         impl.execute(route, req1, context, null);
5550         impl.execute(route, req2, context, null);
5551         verifyMocks();
5552     }
5553 
5554     @Test
5555     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications()
5556     throws Exception {
5557         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5558                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5559         final HttpResponse resp1 = HttpTestUtils.make200Response();
5560         resp1.setHeader("ETag","\"etag\"");
5561         resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
5562 
5563         backendExpectsAnyRequestAndReturn(resp1);
5564 
5565         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5566                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5567         req2.setHeader("Cache-Control","max-stale=7200");
5568         final HttpResponse resp2 = HttpTestUtils.make200Response();
5569 
5570         // this MUST happen
5571         backendExpectsAnyRequestAndReturn(resp2);
5572 
5573         replayMocks();
5574         impl.execute(route, req1, context, null);
5575         impl.execute(route, req2, context, null);
5576         verifyMocks();
5577     }
5578 
5579     /* "If the no-cache directive does specify one or more field-names, then
5580      * a cache MAY use the response to satisfy a subsequent request, subject
5581      * to any other restrictions on caching. However, the specified
5582      * field-name(s) MUST NOT be sent in the response to a subsequent request
5583      * without successful revalidation with the origin server."
5584      */
5585     @Test
5586     public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation()
5587     throws Exception {
5588         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5589                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5590         final HttpResponse resp1 = HttpTestUtils.make200Response();
5591         resp1.setHeader("ETag","\"etag\"");
5592         resp1.setHeader("X-Stuff","things");
5593         resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
5594 
5595         backendExpectsAnyRequestAndReturn(resp1);
5596 
5597         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5598                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5599         final HttpResponse resp2 = HttpTestUtils.make200Response();
5600         resp2.setHeader("ETag","\"etag\"");
5601         resp2.setHeader("X-Stuff","things");
5602         resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
5603 
5604         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5605         EasyMock.expect(
5606                 mockBackend.execute(
5607                         EasyMock.eq(route),
5608                         EasyMock.capture(cap),
5609                         EasyMock.isA(HttpClientContext.class),
5610                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5611                                 Proxies.enhanceResponse(resp2)).times(0,1);
5612 
5613         replayMocks();
5614         impl.execute(route, req1, context, null);
5615         final HttpResponse result = impl.execute(route, req2, context, null);
5616         verifyMocks();
5617 
5618         if (!cap.hasCaptured()) {
5619             Assert.assertNull(result.getFirstHeader("X-Stuff"));
5620         }
5621     }
5622 
5623     /* "The purpose of the no-store directive is to prevent the inadvertent
5624      * release or retention of sensitive information (for example, on backup
5625      * tapes). The no-store directive applies to the entire message, and MAY
5626      * be sent either in a response or in a request. If sent in a request, a
5627      * cache MUST NOT store any part of either this request or any response
5628      * to it. If sent in a response, a cache MUST NOT store any part of
5629      * either this response or the request that elicited it. This directive
5630      * applies to both non- shared and shared caches. "MUST NOT store" in
5631      * this context means that the cache MUST NOT intentionally store the
5632      * information in non-volatile storage, and MUST make a best-effort
5633      * attempt to remove the information from volatile storage as promptly
5634      * as possible after forwarding it."
5635      *
5636      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
5637      */
5638     @Test
5639     public void testNoStoreOnRequestIsNotStoredInCache()
5640     throws Exception {
5641         emptyMockCacheExpectsNoPuts();
5642         request.setHeader("Cache-Control","no-store");
5643         backendExpectsAnyRequest().andReturn(originResponse);
5644 
5645         replayMocks();
5646         impl.execute(route, request, context, null);
5647         verifyMocks();
5648     }
5649 
5650     @Test
5651     public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable()
5652     throws Exception {
5653         emptyMockCacheExpectsNoPuts();
5654         request.setHeader("Cache-Control","no-store");
5655         originResponse.setHeader("Cache-Control","max-age=3600");
5656         backendExpectsAnyRequest().andReturn(originResponse);
5657 
5658         replayMocks();
5659         impl.execute(route, request, context, null);
5660         verifyMocks();
5661     }
5662 
5663     @Test
5664     public void testNoStoreOnResponseIsNotStoredInCache()
5665     throws Exception {
5666         emptyMockCacheExpectsNoPuts();
5667         originResponse.setHeader("Cache-Control","no-store");
5668         backendExpectsAnyRequest().andReturn(originResponse);
5669 
5670         replayMocks();
5671         impl.execute(route, request, context, null);
5672         verifyMocks();
5673     }
5674 
5675     @Test
5676     public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators()
5677     throws Exception {
5678         emptyMockCacheExpectsNoPuts();
5679         originResponse.setHeader("Cache-Control","no-store,max-age=3600");
5680         backendExpectsAnyRequest().andReturn(originResponse);
5681 
5682         replayMocks();
5683         impl.execute(route, request, context, null);
5684         verifyMocks();
5685     }
5686 
5687     /* "If multiple encodings have been applied to an entity, the content
5688      * codings MUST be listed in the order in which they were applied."
5689      *
5690      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
5691      */
5692     @Test
5693     public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved()
5694         throws Exception {
5695         originResponse.addHeader("Content-Encoding","gzip");
5696         originResponse.addHeader("Content-Encoding","deflate");
5697         backendExpectsAnyRequest().andReturn(originResponse);
5698 
5699         replayMocks();
5700         final HttpResponse result = impl.execute(route, request, context, null);
5701         verifyMocks();
5702         int total_encodings = 0;
5703         for(final Header hdr : result.getHeaders("Content-Encoding")) {
5704             for(final HeaderElement elt : hdr.getElements()) {
5705                 switch(total_encodings) {
5706                 case 0:
5707                     Assert.assertEquals("gzip", elt.getName());
5708                     break;
5709                 case 1:
5710                     Assert.assertEquals("deflate", elt.getName());
5711                     break;
5712                 default:
5713                     Assert.fail("too many encodings");
5714                 }
5715                 total_encodings++;
5716             }
5717         }
5718         Assert.assertEquals(2, total_encodings);
5719     }
5720 
5721     @Test
5722     public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved()
5723         throws Exception {
5724         originResponse.addHeader("Content-Encoding","gzip,deflate");
5725         backendExpectsAnyRequest().andReturn(originResponse);
5726 
5727         replayMocks();
5728         final HttpResponse result = impl.execute(route, request, context, null);
5729         verifyMocks();
5730         int total_encodings = 0;
5731         for(final Header hdr : result.getHeaders("Content-Encoding")) {
5732             for(final HeaderElement elt : hdr.getElements()) {
5733                 switch(total_encodings) {
5734                 case 0:
5735                     Assert.assertEquals("gzip", elt.getName());
5736                     break;
5737                 case 1:
5738                     Assert.assertEquals("deflate", elt.getName());
5739                     break;
5740                 default:
5741                     Assert.fail("too many encodings");
5742                 }
5743                 total_encodings++;
5744             }
5745         }
5746         Assert.assertEquals(2, total_encodings);
5747     }
5748 
5749     /* "A cache cannot assume that an entity with a Content-Location
5750      * different from the URI used to retrieve it can be used to respond
5751      * to later requests on that Content-Location URI."
5752      *
5753      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.14
5754      */
5755     @Test
5756     public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource()
5757         throws Exception {
5758         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5759                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
5760         final HttpResponse resp1 = HttpTestUtils.make200Response();
5761         resp1.setHeader("Cache-Control","public,max-age=3600");
5762         resp1.setHeader("Etag","\"etag\"");
5763         resp1.setHeader("Content-Location","http://foo.example.com/bar");
5764 
5765         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5766                 new BasicHttpRequest("GET", "/bar", HttpVersion.HTTP_1_1));
5767         final HttpResponse resp2 = HttpTestUtils.make200Response();
5768         resp2.setHeader("Cache-Control","public,max-age=3600");
5769         resp2.setHeader("Etag","\"etag\"");
5770 
5771         backendExpectsAnyRequestAndReturn(resp1);
5772         backendExpectsAnyRequestAndReturn(resp2);
5773 
5774         replayMocks();
5775         impl.execute(route, req1, context, null);
5776         impl.execute(route, req2, context, null);
5777         verifyMocks();
5778     }
5779 
5780     /* "A received message that does not have a Date header field MUST be
5781      * assigned one by the recipient if the message will be cached by that
5782      * recipient or gatewayed via a protocol which requires a Date."
5783      *
5784      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
5785      */
5786     @Test
5787     public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne()
5788         throws Exception {
5789         originResponse.removeHeaders("Date");
5790         originResponse.setHeader("Cache-Control","public");
5791         originResponse.setHeader("ETag","\"etag\"");
5792 
5793         backendExpectsAnyRequest().andReturn(originResponse);
5794 
5795         replayMocks();
5796         final HttpResponse result = impl.execute(route, request, context, null);
5797         verifyMocks();
5798         Assert.assertNotNull(result.getFirstHeader("Date"));
5799     }
5800 
5801     /* "The Expires entity-header field gives the date/time after which the
5802      * response is considered stale.... HTTP/1.1 clients and caches MUST
5803      * treat other invalid date formats, especially including the value '0',
5804      * as in the past (i.e., 'already expired')."
5805      *
5806      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5807      */
5808     private void testInvalidExpiresHeaderIsTreatedAsStale(
5809             final String expiresHeader) throws Exception {
5810         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5811                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5812         final HttpResponse resp1 = HttpTestUtils.make200Response();
5813         resp1.setHeader("Cache-Control","public");
5814         resp1.setHeader("ETag","\"etag\"");
5815         resp1.setHeader("Expires", expiresHeader);
5816 
5817         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5818                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5819         final HttpResponse resp2 = HttpTestUtils.make200Response();
5820 
5821         backendExpectsAnyRequestAndReturn(resp1);
5822         // second request to origin MUST happen
5823         backendExpectsAnyRequestAndReturn(resp2);
5824 
5825         replayMocks();
5826         impl.execute(route, req1, context, null);
5827         impl.execute(route, req2, context, null);
5828         verifyMocks();
5829     }
5830 
5831     @Test
5832     public void testMalformedExpiresHeaderIsTreatedAsStale()
5833         throws Exception {
5834         testInvalidExpiresHeaderIsTreatedAsStale("garbage");
5835     }
5836 
5837     @Test
5838     public void testExpiresZeroHeaderIsTreatedAsStale()
5839         throws Exception {
5840         testInvalidExpiresHeaderIsTreatedAsStale("0");
5841     }
5842 
5843     /* "To mark a response as 'already expired,' an origin server sends
5844      * an Expires date that is equal to the Date header value."
5845      *
5846      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5847      */
5848     @Test
5849     public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale()
5850         throws Exception {
5851         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5852                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5853         final HttpResponse resp1 = HttpTestUtils.make200Response();
5854         resp1.setHeader("Cache-Control","public");
5855         resp1.setHeader("ETag","\"etag\"");
5856         resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
5857 
5858         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5859                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5860         final HttpResponse resp2 = HttpTestUtils.make200Response();
5861 
5862         backendExpectsAnyRequestAndReturn(resp1);
5863         // second request to origin MUST happen
5864         backendExpectsAnyRequestAndReturn(resp2);
5865 
5866         replayMocks();
5867         impl.execute(route, req1, context, null);
5868         impl.execute(route, req2, context, null);
5869         verifyMocks();
5870     }
5871 
5872     /* "If the response is being forwarded through a proxy, the proxy
5873      * application MUST NOT modify the Server response-header."
5874      *
5875      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38
5876      */
5877     @Test
5878     public void testDoesNotModifyServerResponseHeader()
5879         throws Exception {
5880         final String server = "MockServer/1.0";
5881         originResponse.setHeader("Server", server);
5882 
5883         backendExpectsAnyRequest().andReturn(originResponse);
5884 
5885         replayMocks();
5886         final HttpResponse result = impl.execute(route, request, context, null);
5887         verifyMocks();
5888         Assert.assertEquals(server, result.getFirstHeader("Server").getValue());
5889     }
5890 
5891     /* "If multiple encodings have been applied to an entity, the transfer-
5892      * codings MUST be listed in the order in which they were applied."
5893      *
5894      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
5895      */
5896     @Test
5897     public void testOrderOfMultipleTransferEncodingHeadersIsPreserved()
5898         throws Exception {
5899         originResponse.addHeader("Transfer-Encoding","chunked");
5900         originResponse.addHeader("Transfer-Encoding","x-transfer");
5901 
5902         backendExpectsAnyRequest().andReturn(originResponse);
5903 
5904         replayMocks();
5905         final HttpResponse result = impl.execute(route, request, context, null);
5906         verifyMocks();
5907         int transfer_encodings = 0;
5908         for(final Header h : result.getHeaders("Transfer-Encoding")) {
5909             for(final HeaderElement elt : h.getElements()) {
5910                 switch(transfer_encodings) {
5911                 case 0:
5912                     Assert.assertEquals("chunked",elt.getName());
5913                     break;
5914                 case 1:
5915                     Assert.assertEquals("x-transfer",elt.getName());
5916                     break;
5917                 default:
5918                     Assert.fail("too many transfer encodings");
5919                 }
5920                 transfer_encodings++;
5921             }
5922         }
5923         Assert.assertEquals(2, transfer_encodings);
5924     }
5925 
5926     @Test
5927     public void testOrderOfMultipleTransferEncodingsInSingleHeadersIsPreserved()
5928         throws Exception {
5929         originResponse.addHeader("Transfer-Encoding","chunked, x-transfer");
5930 
5931         backendExpectsAnyRequest().andReturn(originResponse);
5932 
5933         replayMocks();
5934         final HttpResponse result = impl.execute(route, request, context, null);
5935         verifyMocks();
5936         int transfer_encodings = 0;
5937         for(final Header h : result.getHeaders("Transfer-Encoding")) {
5938             for(final HeaderElement elt : h.getElements()) {
5939                 switch(transfer_encodings) {
5940                 case 0:
5941                     Assert.assertEquals("chunked",elt.getName());
5942                     break;
5943                 case 1:
5944                     Assert.assertEquals("x-transfer",elt.getName());
5945                     break;
5946                 default:
5947                     Assert.fail("too many transfer encodings");
5948                 }
5949                 transfer_encodings++;
5950             }
5951         }
5952         Assert.assertEquals(2, transfer_encodings);
5953     }
5954 
5955     /* "A Vary field value of '*' signals that unspecified parameters
5956      * not limited to the request-headers (e.g., the network address
5957      * of the client), play a role in the selection of the response
5958      * representation. The '*' value MUST NOT be generated by a proxy
5959      * server; it may only be generated by an origin server."
5960      *
5961      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
5962      */
5963     @Test
5964     public void testVaryStarIsNotGeneratedByProxy()
5965         throws Exception {
5966         request.setHeader("User-Agent","my-agent/1.0");
5967         originResponse.setHeader("Cache-Control","public, max-age=3600");
5968         originResponse.setHeader("Vary","User-Agent");
5969         originResponse.setHeader("ETag","\"etag\"");
5970 
5971         backendExpectsAnyRequest().andReturn(originResponse);
5972 
5973         replayMocks();
5974         final HttpResponse result = impl.execute(route, request, context, null);
5975         verifyMocks();
5976         for(final Header h : result.getHeaders("Vary")) {
5977             for(final HeaderElement elt : h.getElements()) {
5978                 Assert.assertFalse("*".equals(elt.getName()));
5979             }
5980         }
5981     }
5982 
5983     /* "The Via general-header field MUST be used by gateways and proxies
5984      * to indicate the intermediate protocols and recipients between the
5985      * user agent and the server on requests, and between the origin server
5986      * and the client on responses."
5987      *
5988      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
5989      */
5990     @Test
5991     public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
5992         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5993         request.removeHeaders("Via");
5994         EasyMock.expect(
5995                 mockBackend.execute(
5996                         EasyMock.isA(HttpRoute.class),
5997                         EasyMock.capture(cap),
5998                         EasyMock.isA(HttpClientContext.class),
5999                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
6000 
6001         replayMocks();
6002         impl.execute(route, request, context, null);
6003         verifyMocks();
6004 
6005         final HttpRequest captured = cap.getValue();
6006         final String via = captured.getFirstHeader("Via").getValue();
6007         assertValidViaHeader(via);
6008     }
6009 
6010     @Test
6011     public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception {
6012         originResponse.removeHeaders("Via");
6013         backendExpectsAnyRequest().andReturn(originResponse);
6014         replayMocks();
6015         final HttpResponse result = impl.execute(route, request, context, null);
6016         verifyMocks();
6017         assertValidViaHeader(result.getFirstHeader("Via").getValue());
6018     }
6019 
6020 
6021     private void assertValidViaHeader(final String via) {
6022         //        Via =  "Via" ":" 1#( received-protocol received-by [ comment ] )
6023         //        received-protocol = [ protocol-name "/" ] protocol-version
6024         //        protocol-name     = token
6025         //        protocol-version  = token
6026         //        received-by       = ( host [ ":" port ] ) | pseudonym
6027         //        pseudonym         = token
6028 
6029         final String[] parts = via.split("\\s+");
6030         Assert.assertTrue(parts.length >= 2);
6031 
6032         // received protocol
6033         final String receivedProtocol = parts[0];
6034         final String[] protocolParts = receivedProtocol.split("/");
6035         Assert.assertTrue(protocolParts.length >= 1);
6036         Assert.assertTrue(protocolParts.length <= 2);
6037 
6038         final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+";
6039         for(final String protocolPart : protocolParts) {
6040             Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart));
6041         }
6042 
6043         // received-by
6044         if (!Pattern.matches(tokenRegexp, parts[1])) {
6045             // host : port
6046             new HttpHost(parts[1]); // TODO - unused - is this a test bug? else use Assert.assertNotNull
6047         }
6048 
6049         // comment
6050         if (parts.length > 2) {
6051             final StringBuilder buf = new StringBuilder(parts[2]);
6052             for(int i=3; i<parts.length; i++) {
6053                 buf.append(" "); buf.append(parts[i]);
6054             }
6055             Assert.assertTrue(isValidComment(buf.toString()));
6056         }
6057     }
6058 
6059     private boolean isValidComment(final String s) {
6060         final String leafComment = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
6061         final String nestedPrefix = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\(";
6062         final String nestedSuffix = "\\)([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
6063 
6064         if (Pattern.matches(leafComment,s)) {
6065             return true;
6066         }
6067         final Matcher pref = Pattern.compile(nestedPrefix).matcher(s);
6068         final Matcher suff = Pattern.compile(nestedSuffix).matcher(s);
6069         if (!pref.find()) {
6070             return false;
6071         }
6072         if (!suff.find()) {
6073             return false;
6074         }
6075         return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
6076     }
6077 
6078 
6079     /*
6080      * "The received-protocol indicates the protocol version of the message
6081      * received by the server or client along each segment of the request/
6082      * response chain. The received-protocol version is appended to the Via
6083      * field value when the message is forwarded so that information about
6084      * the protocol capabilities of upstream applications remains visible
6085      * to all recipients."
6086      *
6087      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
6088      */
6089     @Test
6090     public void testViaHeaderOnRequestProperlyRecordsClientProtocol()
6091     throws Exception {
6092         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0));
6093         request.removeHeaders("Via");
6094         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
6095         EasyMock.expect(
6096                 mockBackend.execute(
6097                         EasyMock.isA(HttpRoute.class),
6098                         EasyMock.capture(cap),
6099                         EasyMock.isA(HttpClientContext.class),
6100                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
6101 
6102         replayMocks();
6103         impl.execute(route, request, context, null);
6104         verifyMocks();
6105 
6106         final HttpRequest captured = cap.getValue();
6107         final String via = captured.getFirstHeader("Via").getValue();
6108         final String protocol = via.split("\\s+")[0];
6109         final String[] protoParts = protocol.split("/");
6110         if (protoParts.length > 1) {
6111             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
6112         }
6113         Assert.assertEquals("1.0",protoParts[protoParts.length-1]);
6114     }
6115 
6116     @Test
6117     public void testViaHeaderOnResponseProperlyRecordsOriginProtocol()
6118     throws Exception {
6119 
6120         originResponse = Proxies.enhanceResponse(
6121                 new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content"));
6122 
6123         backendExpectsAnyRequest().andReturn(originResponse);
6124 
6125         replayMocks();
6126         final HttpResponse result = impl.execute(route, request, context, null);
6127         verifyMocks();
6128 
6129         final String via = result.getFirstHeader("Via").getValue();
6130         final String protocol = via.split("\\s+")[0];
6131         final String[] protoParts = protocol.split("/");
6132         Assert.assertTrue(protoParts.length >= 1);
6133         Assert.assertTrue(protoParts.length <= 2);
6134         if (protoParts.length > 1) {
6135             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
6136         }
6137         Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
6138     }
6139 
6140     /* "A cache MUST NOT delete any Warning header that it received with
6141      * a message."
6142      *
6143      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6144      */
6145     @Test
6146     public void testRetainsWarningHeadersReceivedFromUpstream()
6147         throws Exception {
6148         originResponse.removeHeaders("Warning");
6149         final String warning = "199 fred \"misc\"";
6150         originResponse.addHeader("Warning", warning);
6151         backendExpectsAnyRequest().andReturn(originResponse);
6152 
6153         replayMocks();
6154         final HttpResponse result = impl.execute(route, request, context, null);
6155         verifyMocks();
6156         Assert.assertEquals(warning,
6157                 result.getFirstHeader("Warning").getValue());
6158     }
6159 
6160     /* "However, if a cache successfully validates a cache entry, it
6161      * SHOULD remove any Warning headers previously attached to that
6162      * entry except as specified for specific Warning codes. It MUST
6163      * then add any Warning headers received in the validating response."
6164      *
6165      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6166      */
6167     @Test
6168     public void testUpdatesWarningHeadersOnValidation()
6169         throws Exception {
6170         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
6171                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
6172         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
6173                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
6174 
6175         final Date now = new Date();
6176         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
6177         final HttpResponse resp1 = HttpTestUtils.make200Response();
6178         resp1.setHeader("Date", DateUtils.formatDate(twentySecondsAgo));
6179         resp1.setHeader("Cache-Control","public,max-age=5");
6180         resp1.setHeader("ETag", "\"etag1\"");
6181         final String oldWarning = "113 wilma \"stale\"";
6182         resp1.setHeader("Warning", oldWarning);
6183 
6184         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6185         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
6186         resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
6187         resp2.setHeader("ETag", "\"etag1\"");
6188         final String newWarning = "113 betty \"stale too\"";
6189         resp2.setHeader("Warning", newWarning);
6190 
6191         backendExpectsAnyRequestAndReturn(resp1);
6192         backendExpectsAnyRequestAndReturn(resp2);
6193 
6194         replayMocks();
6195         impl.execute(route, req1, context, null);
6196         final HttpResponse result = impl.execute(route, req2, context, null);
6197         verifyMocks();
6198 
6199         boolean oldWarningFound = false;
6200         boolean newWarningFound = false;
6201         for(final Header h : result.getHeaders("Warning")) {
6202             for(final String warnValue : h.getValue().split("\\s*,\\s*")) {
6203                 if (oldWarning.equals(warnValue)) {
6204                     oldWarningFound = true;
6205                 } else if (newWarning.equals(warnValue)) {
6206                     newWarningFound = true;
6207                 }
6208             }
6209         }
6210         Assert.assertFalse(oldWarningFound);
6211         Assert.assertTrue(newWarningFound);
6212     }
6213 
6214     /* "If an implementation sends a message with one or more Warning
6215      * headers whose version is HTTP/1.0 or lower, then the sender MUST
6216      * include in each warning-value a warn-date that matches the date
6217      * in the response."
6218      *
6219      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6220      */
6221     @Test
6222     public void testWarnDatesAreAddedToWarningsOnLowerProtocolVersions()
6223         throws Exception {
6224         final String dateHdr = DateUtils.formatDate(new Date());
6225         final String origWarning = "110 fred \"stale\"";
6226         originResponse.setStatusLine(HttpVersion.HTTP_1_0, HttpStatus.SC_OK);
6227         originResponse.addHeader("Warning", origWarning);
6228         originResponse.setHeader("Date", dateHdr);
6229         backendExpectsAnyRequest().andReturn(originResponse);
6230         replayMocks();
6231         final HttpResponse result = impl.execute(route, request, context, null);
6232         verifyMocks();
6233         // note that currently the implementation acts as an HTTP/1.1 proxy,
6234         // which means that all the responses from the caching module should
6235         // be HTTP/1.1, so we won't actually be testing anything here until
6236         // that changes.
6237         if (HttpVersion.HTTP_1_0.greaterEquals(result.getProtocolVersion())) {
6238             Assert.assertEquals(dateHdr, result.getFirstHeader("Date").getValue());
6239             boolean warningFound = false;
6240             final String targetWarning = origWarning + " \"" + dateHdr + "\"";
6241             for(final Header h : result.getHeaders("Warning")) {
6242                 for(final String warning : h.getValue().split("\\s*,\\s*")) {
6243                     if (targetWarning.equals(warning)) {
6244                         warningFound = true;
6245                         break;
6246                     }
6247                 }
6248             }
6249             Assert.assertTrue(warningFound);
6250         }
6251     }
6252 
6253     /* "If an implementation receives a message with a warning-value that
6254      * includes a warn-date, and that warn-date is different from the Date
6255      * value in the response, then that warning-value MUST be deleted from
6256      * the message before storing, forwarding, or using it. (This prevents
6257      * bad consequences of naive caching of Warning header fields.) If all
6258      * of the warning-values are deleted for this reason, the Warning
6259      * header MUST be deleted as well."
6260      *
6261      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6262      */
6263     @Test
6264     public void testStripsBadlyDatedWarningsFromForwardedResponses()
6265         throws Exception {
6266         final Date now = new Date();
6267         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6268         originResponse.setHeader("Date", DateUtils.formatDate(now));
6269         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
6270                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6271         originResponse.setHeader("Cache-Control","no-cache,no-store");
6272         backendExpectsAnyRequest().andReturn(originResponse);
6273 
6274         replayMocks();
6275         final HttpResponse result = impl.execute(route, request, context, null);
6276         verifyMocks();
6277 
6278         for(final Header h : result.getHeaders("Warning")) {
6279             Assert.assertFalse(h.getValue().contains("wilma"));
6280         }
6281     }
6282 
6283     @Test
6284     public void testStripsBadlyDatedWarningsFromStoredResponses()
6285         throws Exception {
6286         final Date now = new Date();
6287         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6288         originResponse.setHeader("Date", DateUtils.formatDate(now));
6289         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
6290                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6291         originResponse.setHeader("Cache-Control","public,max-age=3600");
6292         backendExpectsAnyRequest().andReturn(originResponse);
6293 
6294         replayMocks();
6295         final HttpResponse result = impl.execute(route, request, context, null);
6296         verifyMocks();
6297 
6298         for(final Header h : result.getHeaders("Warning")) {
6299             Assert.assertFalse(h.getValue().contains("wilma"));
6300         }
6301     }
6302 
6303     @Test
6304     public void testRemovesWarningHeaderIfAllWarnValuesAreBadlyDated()
6305     throws Exception {
6306         final Date now = new Date();
6307         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6308         originResponse.setHeader("Date", DateUtils.formatDate(now));
6309         originResponse.addHeader("Warning", "110 wilma \"stale\" \""
6310                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6311         backendExpectsAnyRequest().andReturn(originResponse);
6312 
6313         replayMocks();
6314         final HttpResponse result = impl.execute(route, request, context, null);
6315         verifyMocks();
6316 
6317         final Header[] warningHeaders = result.getHeaders("Warning");
6318         Assert.assertTrue(warningHeaders == null || warningHeaders.length == 0);
6319     }
6320 
6321 }