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