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  
30  import static org.junit.jupiter.api.Assertions.assertSame;
31  
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.net.SocketException;
35  import java.net.SocketTimeoutException;
36  import java.time.Instant;
37  import java.time.temporal.ChronoUnit;
38  
39  import org.apache.hc.client5.http.HttpRoute;
40  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
41  import org.apache.hc.client5.http.auth.StandardAuthScheme;
42  import org.apache.hc.client5.http.cache.CacheResponseStatus;
43  import org.apache.hc.client5.http.cache.HttpCacheContext;
44  import org.apache.hc.client5.http.cache.HttpCacheEntry;
45  import org.apache.hc.client5.http.cache.HttpCacheStorage;
46  import org.apache.hc.client5.http.classic.ExecChain;
47  import org.apache.hc.client5.http.classic.ExecRuntime;
48  import org.apache.hc.client5.http.classic.methods.HttpGet;
49  import org.apache.hc.client5.http.classic.methods.HttpOptions;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.utils.DateUtils;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.HttpException;
55  import org.apache.hc.core5.http.HttpHost;
56  import org.apache.hc.core5.http.HttpRequest;
57  import org.apache.hc.core5.http.HttpStatus;
58  import org.apache.hc.core5.http.HttpVersion;
59  import org.apache.hc.core5.http.io.entity.EntityUtils;
60  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
61  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
62  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
63  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
64  import org.apache.hc.core5.net.URIAuthority;
65  import org.junit.jupiter.api.Assertions;
66  import org.junit.jupiter.api.BeforeEach;
67  import org.junit.jupiter.api.Test;
68  import org.mockito.ArgumentCaptor;
69  import org.mockito.Mock;
70  import org.mockito.Mockito;
71  import org.mockito.MockitoAnnotations;
72  import org.mockito.Spy;
73  
74  public class TestCachingExecChain {
75  
76      @Mock
77      ExecChain mockExecChain;
78      @Mock
79      ExecRuntime mockExecRuntime;
80      @Mock
81      HttpCacheStorage mockStorage;
82      @Spy
83      HttpCache cache = new BasicHttpCache();
84  
85      CacheConfig config;
86      HttpRoute route;
87      HttpHost host;
88      ClassicHttpRequest request;
89      HttpCacheContext context;
90      HttpCacheEntry entry;
91      CachingExec impl;
92  
93      @BeforeEach
94      public void setUp() {
95          MockitoAnnotations.openMocks(this);
96          config = CacheConfig.DEFAULT;
97  
98          host = new HttpHost("foo.example.com", 80);
99          route = new HttpRoute(host);
100         request = new BasicClassicHttpRequest("GET", "/stuff");
101         context = HttpCacheContext.create();
102         entry = HttpTestUtils.makeCacheEntry();
103 
104         impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
105     }
106 
107     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
108         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
109         return impl.execute(ClassicRequestBuilder.copy(request).build(), scope, mockExecChain);
110     }
111 
112     @Test
113     public void testCacheableResponsesGoIntoCache() throws Exception {
114         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
115         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
116         resp1.setHeader("Cache-Control", "max-age=3600");
117 
118         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
119 
120         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
121 
122         execute(req1);
123         execute(req2);
124 
125         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
126         Mockito.verify(cache).createCacheEntry(Mockito.eq(host), RequestEquivalent.eq(req1),
127                 Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
128     }
129 
130     @Test
131     public void testOlderCacheableResponsesDoNotGoIntoCache() throws Exception {
132         final Instant now = Instant.now();
133         final Instant fiveSecondsAgo = now.minusSeconds(5);
134 
135         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
136         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
137         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
138         resp1.setHeader("Cache-Control", "max-age=3600");
139         resp1.setHeader("Etag", "\"new-etag\"");
140 
141         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
142         req2.setHeader("Cache-Control", "no-cache");
143         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
144         resp2.setHeader("ETag", "\"old-etag\"");
145         resp2.setHeader("Date", DateUtils.formatStandardDate(fiveSecondsAgo));
146         resp2.setHeader("Cache-Control", "max-age=3600");
147 
148         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
149 
150         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
151 
152         execute(req1);
153 
154         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
155 
156         execute(req2);
157         final ClassicHttpResponse result = execute(req3);
158 
159         Assertions.assertEquals("\"new-etag\"", result.getFirstHeader("ETag").getValue());
160     }
161 
162     @Test
163     public void testNewerCacheableResponsesReplaceExistingCacheEntry() throws Exception {
164         final Instant now = Instant.now();
165         final Instant fiveSecondsAgo = now.minusSeconds(5);
166 
167         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
168         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
169         resp1.setHeader("Date", DateUtils.formatStandardDate(fiveSecondsAgo));
170         resp1.setHeader("Cache-Control", "max-age=3600");
171         resp1.setHeader("Etag", "\"old-etag\"");
172 
173         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
174         req2.setHeader("Cache-Control", "max-age=0");
175         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
176         resp2.setHeader("ETag", "\"new-etag\"");
177         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
178         resp2.setHeader("Cache-Control", "max-age=3600");
179 
180         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
181 
182         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
183 
184         execute(req1);
185 
186         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
187 
188         execute(req2);
189         final ClassicHttpResponse result = execute(req3);
190 
191         Assertions.assertEquals("\"new-etag\"", result.getFirstHeader("ETag").getValue());
192     }
193 
194     @Test
195     public void testNonCacheableResponseIsNotCachedAndIsReturnedAsIs() throws Exception {
196         final HttpCache cache = new BasicHttpCache(new HeapResourceFactory(), mockStorage);
197         impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
198 
199         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
200         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
201         resp1.setHeader("Cache-Control", "no-cache");
202 
203         Mockito.when(mockStorage.getEntry(Mockito.any())).thenReturn(null);
204         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
205 
206         final ClassicHttpResponse result = execute(req1);
207 
208         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
209 
210         Mockito.verify(mockStorage, Mockito.never()).putEntry(Mockito.any(), Mockito.any());
211     }
212 
213     @Test
214     public void testSetsModuleGeneratedResponseContextForCacheOptionsResponse() throws Exception {
215         final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS", "*");
216         req.setHeader("Max-Forwards", "0");
217 
218         execute(req);
219         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
220     }
221 
222     @Test
223     public void testSetsModuleGeneratedResponseContextForFatallyNoncompliantRequest() throws Exception {
224         final ClassicHttpRequest req = new HttpGet("http://foo.example.com/");
225         req.setHeader("Range", "bytes=0-50");
226         req.setHeader("If-Range", "W/\"weak-etag\"");
227 
228         execute(req);
229         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
230     }
231 
232     @Test
233     public void testRecordsClientProtocolInViaHeaderIfRequestNotServableFromCache() throws Exception {
234         final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/");
235         originalRequest.setVersion(HttpVersion.HTTP_1_0);
236         final ClassicHttpRequest req = originalRequest;
237         req.setHeader("Cache-Control", "no-cache");
238         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
239 
240         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
241 
242         execute(req);
243 
244         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
245         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
246 
247         final HttpRequest captured = reqCapture.getValue();
248         final String via = captured.getFirstHeader("Via").getValue();
249         final String proto = via.split("\\s+")[0];
250         Assertions.assertTrue("http/1.0".equalsIgnoreCase(proto) || "1.0".equalsIgnoreCase(proto));
251     }
252 
253     @Test
254     public void testSetsCacheMissContextIfRequestNotServableFromCache() throws Exception {
255         final ClassicHttpRequest req = new HttpGet("http://foo.example.com/");
256         req.setHeader("Cache-Control", "no-cache");
257         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
258 
259         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
260 
261         execute(req);
262         Assertions.assertEquals(CacheResponseStatus.CACHE_MISS, context.getCacheResponseStatus());
263     }
264 
265     @Test
266     public void testSetsViaHeaderOnResponseIfRequestNotServableFromCache() throws Exception {
267         final ClassicHttpRequest req = new HttpGet("http://foo.example.com/");
268         req.setHeader("Cache-Control", "no-cache");
269         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
270 
271         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
272 
273         final ClassicHttpResponse result = execute(req);
274         Assertions.assertNotNull(result.getFirstHeader("Via"));
275     }
276 
277     @Test
278     public void testSetsViaHeaderOnResponseForCacheMiss() throws Exception {
279         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
280         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
281         resp1.setEntity(HttpTestUtils.makeBody(128));
282         resp1.setHeader("Content-Length", "128");
283         resp1.setHeader("ETag", "\"etag\"");
284         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
285         resp1.setHeader("Cache-Control", "public, max-age=3600");
286 
287         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
288 
289         final ClassicHttpResponse result = execute(req1);
290         Assertions.assertNotNull(result.getFirstHeader("Via"));
291     }
292 
293     @Test
294     public void testSetsCacheHitContextIfRequestServedFromCache() throws Exception {
295         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
296         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
297         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
298         resp1.setEntity(HttpTestUtils.makeBody(128));
299         resp1.setHeader("Content-Length", "128");
300         resp1.setHeader("ETag", "\"etag\"");
301         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
302         resp1.setHeader("Cache-Control", "public, max-age=3600");
303 
304         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
305 
306         execute(req1);
307         execute(req2);
308         Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus());
309     }
310 
311     @Test
312     public void testSetsViaHeaderOnResponseIfRequestServedFromCache() throws Exception {
313         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
314         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
315         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
316         resp1.setEntity(HttpTestUtils.makeBody(128));
317         resp1.setHeader("Content-Length", "128");
318         resp1.setHeader("ETag", "\"etag\"");
319         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
320         resp1.setHeader("Cache-Control", "public, max-age=3600");
321 
322         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
323 
324         execute(req1);
325         final ClassicHttpResponse result = execute(req2);
326         Assertions.assertNotNull(result.getFirstHeader("Via"));
327     }
328 
329     @Test
330     public void testReturns304ForIfModifiedSinceHeaderIfRequestServedFromCache() throws Exception {
331         final Instant now = Instant.now();
332         final Instant tenSecondsAgo = now.minusSeconds(10);
333         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
334         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
335         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
336         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
337         resp1.setEntity(HttpTestUtils.makeBody(128));
338         resp1.setHeader("Content-Length", "128");
339         resp1.setHeader("ETag", "\"etag\"");
340         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
341         resp1.setHeader("Cache-Control", "public, max-age=3600");
342         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
343 
344         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
345 
346         execute(req1);
347         final ClassicHttpResponse result = execute(req2);
348         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
349     }
350 
351     @Test
352     public void testReturns304ForIfModifiedSinceHeaderIf304ResponseInCache() throws Exception {
353         final Instant now = Instant.now();
354         final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
355         final Instant inTenMinutes = now.plus(10, ChronoUnit.MINUTES);
356         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
357         req1.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
358         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
359         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
360 
361         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
362         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
363         resp1.setHeader("Cache-control", "max-age=600");
364         resp1.setHeader("Expires", DateUtils.formatStandardDate(inTenMinutes));
365 
366         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
367 
368         execute(req1);
369 
370         final ClassicHttpResponse result = execute(req2);
371         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
372         Assertions.assertFalse(result.containsHeader("Last-Modified"));
373 
374         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
375     }
376 
377     @Test
378     public void testReturns200ForIfModifiedSinceDateIsLess() throws Exception {
379         final Instant now = Instant.now();
380         final Instant tenSecondsAgo = now.minusSeconds(10);
381         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
382         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
383 
384         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
385         resp1.setEntity(HttpTestUtils.makeBody(128));
386         resp1.setHeader("Content-Length", "128");
387         resp1.setHeader("ETag", "\"etag\"");
388         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
389         resp1.setHeader("Cache-Control", "public, max-age=3600");
390         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
391 
392         // The variant has been modified since this date
393         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
394 
395         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
396 
397         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
398 
399         execute(req1);
400 
401         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
402 
403         final ClassicHttpResponse result = execute(req2);
404         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
405     }
406 
407     @Test
408     public void testReturns200ForIfModifiedSinceDateIsInvalid() throws Exception {
409         final Instant now = Instant.now();
410         final Instant tenSecondsAfter = now.plusSeconds(10);
411         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
412         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
413 
414         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
415         resp1.setEntity(HttpTestUtils.makeBody(128));
416         resp1.setHeader("Content-Length", "128");
417         resp1.setHeader("ETag", "\"etag\"");
418         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
419         resp1.setHeader("Cache-Control", "public, max-age=3600");
420         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
421 
422         // invalid date (date in the future)
423         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAfter));
424 
425         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
426 
427         execute(req1);
428         final ClassicHttpResponse result = execute(req2);
429         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
430 
431         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
432     }
433 
434     @Test
435     public void testReturns304ForIfNoneMatchHeaderIfRequestServedFromCache() throws Exception {
436         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
437         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
438         req2.addHeader("If-None-Match", "*");
439         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
440         resp1.setEntity(HttpTestUtils.makeBody(128));
441         resp1.setHeader("Content-Length", "128");
442         resp1.setHeader("ETag", "\"etag\"");
443         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
444         resp1.setHeader("Cache-Control", "public, max-age=3600");
445 
446         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
447 
448         execute(req1);
449         final ClassicHttpResponse result = execute(req2);
450         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
451 
452     }
453 
454     @Test
455     public void testReturns200ForIfNoneMatchHeaderFails() throws Exception {
456         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
457         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
458 
459         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
460         resp1.setEntity(HttpTestUtils.makeBody(128));
461         resp1.setHeader("Content-Length", "128");
462         resp1.setHeader("ETag", "\"etag\"");
463         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
464         resp1.setHeader("Cache-Control", "public, max-age=3600");
465 
466         req2.addHeader("If-None-Match", "\"abc\"");
467 
468         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
469 
470         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
471 
472         execute(req1);
473 
474         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
475 
476         final ClassicHttpResponse result = execute(req2);
477         Assertions.assertEquals(200, result.getCode());
478     }
479 
480     @Test
481     public void testReturns304ForIfNoneMatchHeaderAndIfModifiedSinceIfRequestServedFromCache() throws Exception {
482         final Instant now = Instant.now();
483         final Instant tenSecondsAgo = now.minusSeconds(10);
484         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
485         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
486 
487         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
488         resp1.setEntity(HttpTestUtils.makeBody(128));
489         resp1.setHeader("Content-Length", "128");
490         resp1.setHeader("ETag", "\"etag\"");
491         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
492         resp1.setHeader("Cache-Control", "public, max-age=3600");
493         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
494 
495         req2.addHeader("If-None-Match", "*");
496         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
497 
498         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
499 
500         execute(req1);
501         final ClassicHttpResponse result = execute(req2);
502         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
503     }
504 
505     @Test
506     public void testReturns200ForIfNoneMatchHeaderFailsIfModifiedSinceIgnored() throws Exception {
507         final Instant now = Instant.now();
508         final Instant tenSecondsAgo = now.minusSeconds(10);
509         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
510         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
511         req2.addHeader("If-None-Match", "\"abc\"");
512         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
513         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
514         resp1.setEntity(HttpTestUtils.makeBody(128));
515         resp1.setHeader("Content-Length", "128");
516         resp1.setHeader("ETag", "\"etag\"");
517         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
518         resp1.setHeader("Cache-Control", "public, max-age=3600");
519         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
520 
521         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
522 
523         execute(req1);
524         final ClassicHttpResponse result = execute(req2);
525         Assertions.assertEquals(200, result.getCode());
526     }
527 
528     @Test
529     public void testReturns200ForOptionsFollowedByGetIfAuthorizationHeaderAndSharedCache() throws Exception {
530         impl = new CachingExec(cache, null, CacheConfig.custom().setSharedCache(true).build());
531         final Instant now = Instant.now();
532         final ClassicHttpRequest req1 = new HttpOptions("http://foo.example.com/");
533         req1.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
534         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
535         req2.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
536         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
537         resp1.setHeader("Content-Length", "0");
538         resp1.setHeader("ETag", "\"options-etag\"");
539         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
540         resp1.setHeader("Cache-Control", "public, max-age=3600");
541         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
542         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
543         resp1.setEntity(HttpTestUtils.makeBody(128));
544         resp1.setHeader("Content-Length", "128");
545         resp1.setHeader("ETag", "\"get-etag\"");
546         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
547         resp1.setHeader("Cache-Control", "public, max-age=3600");
548         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
549 
550         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
551         execute(req1);
552 
553         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
554 
555         final ClassicHttpResponse result = execute(req2);
556         Assertions.assertEquals(200, result.getCode());
557     }
558 
559     @Test
560     public void testSetsValidatedContextIfRequestWasSuccessfullyValidated() throws Exception {
561         final Instant now = Instant.now();
562         final Instant tenSecondsAgo = now.minusSeconds(10);
563 
564         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
565         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
566 
567         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
568         resp1.setEntity(HttpTestUtils.makeBody(128));
569         resp1.setHeader("Content-Length", "128");
570         resp1.setHeader("ETag", "\"etag\"");
571         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
572         resp1.setHeader("Cache-Control", "public, max-age=5");
573 
574         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
575         resp2.setEntity(HttpTestUtils.makeBody(128));
576         resp2.setHeader("Content-Length", "128");
577         resp2.setHeader("ETag", "\"etag\"");
578         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
579         resp2.setHeader("Cache-Control", "public, max-age=5");
580 
581         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
582         execute(req1);
583 
584         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
585 
586         execute(req2);
587         Assertions.assertEquals(CacheResponseStatus.VALIDATED, context.getCacheResponseStatus());
588     }
589 
590     @Test
591     public void testSetsViaHeaderIfRequestWasSuccessfullyValidated() throws Exception {
592         final Instant now = Instant.now();
593         final Instant tenSecondsAgo = now.minusSeconds(10);
594 
595         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
596         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
597 
598         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
599         resp1.setEntity(HttpTestUtils.makeBody(128));
600         resp1.setHeader("Content-Length", "128");
601         resp1.setHeader("ETag", "\"etag\"");
602         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
603         resp1.setHeader("Cache-Control", "public, max-age=5");
604 
605         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
606         resp2.setEntity(HttpTestUtils.makeBody(128));
607         resp2.setHeader("Content-Length", "128");
608         resp2.setHeader("ETag", "\"etag\"");
609         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
610         resp2.setHeader("Cache-Control", "public, max-age=5");
611 
612         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
613 
614         execute(req1);
615 
616         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
617 
618         final ClassicHttpResponse result = execute(req2);
619         Assertions.assertNotNull(result.getFirstHeader("Via"));
620     }
621 
622     @Test
623     public void testSetsModuleResponseContextIfValidationRequiredButFailed() throws Exception {
624         final Instant now = Instant.now();
625         final Instant tenSecondsAgo = now.minusSeconds(10);
626 
627         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
628         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
629 
630         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
631         resp1.setEntity(HttpTestUtils.makeBody(128));
632         resp1.setHeader("Content-Length", "128");
633         resp1.setHeader("ETag", "\"etag\"");
634         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
635         resp1.setHeader("Cache-Control", "public, max-age=5, must-revalidate");
636 
637         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
638 
639         execute(req1);
640 
641         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
642 
643         execute(req2);
644         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
645             context.getCacheResponseStatus());
646     }
647 
648     @Test
649     public void testSetsModuleResponseContextIfValidationFailsButNotRequired() throws Exception {
650         final Instant now = Instant.now();
651         final Instant tenSecondsAgo = now.minusSeconds(10);
652 
653         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
654         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
655 
656         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
657         resp1.setEntity(HttpTestUtils.makeBody(128));
658         resp1.setHeader("Content-Length", "128");
659         resp1.setHeader("ETag", "\"etag\"");
660         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
661         resp1.setHeader("Cache-Control", "public, max-age=5");
662 
663         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
664 
665         execute(req1);
666 
667         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
668 
669         execute(req2);
670         Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus());
671     }
672 
673     @Test
674     public void testSetViaHeaderIfValidationFailsButNotRequired() throws Exception {
675         final Instant now = Instant.now();
676         final Instant tenSecondsAgo = now.minusSeconds(10);
677 
678         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
679         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
680 
681         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
682         resp1.setEntity(HttpTestUtils.makeBody(128));
683         resp1.setHeader("Content-Length", "128");
684         resp1.setHeader("ETag", "\"etag\"");
685         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
686         resp1.setHeader("Cache-Control", "public, max-age=5");
687 
688         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
689 
690         execute(req1);
691 
692         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
693 
694         final ClassicHttpResponse result = execute(req2);
695         Assertions.assertNotNull(result.getFirstHeader("Via"));
696     }
697 
698     @Test
699     public void testReturns304ForIfNoneMatchPassesIfRequestServedFromOrigin() throws Exception {
700 
701         final Instant now = Instant.now();
702         final Instant tenSecondsAgo = now.minusSeconds(10);
703 
704         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
705         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
706 
707         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
708         resp1.setEntity(HttpTestUtils.makeBody(128));
709         resp1.setHeader("Content-Length", "128");
710         resp1.setHeader("ETag", "\"etag\"");
711         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
712         resp1.setHeader("Cache-Control", "public, max-age=5");
713 
714         req2.addHeader("If-None-Match", "\"etag\"");
715         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
716         resp2.setHeader("ETag", "\"etag\"");
717         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
718         resp2.setHeader("Cache-Control", "public, max-age=5");
719 
720         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
721         execute(req1);
722 
723         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
724 
725         final ClassicHttpResponse result = execute(req2);
726 
727         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
728     }
729 
730     @Test
731     public void testReturns200ForIfNoneMatchFailsIfRequestServedFromOrigin() throws Exception {
732 
733         final Instant now = Instant.now();
734         final Instant tenSecondsAgo = now.minusSeconds(10);
735 
736         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
737         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
738 
739         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
740         resp1.setEntity(HttpTestUtils.makeBody(128));
741         resp1.setHeader("Content-Length", "128");
742         resp1.setHeader("ETag", "\"etag\"");
743         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
744         resp1.setHeader("Cache-Control", "public, max-age=5");
745 
746         req2.addHeader("If-None-Match", "\"etag\"");
747         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
748         resp2.setEntity(HttpTestUtils.makeBody(128));
749         resp2.setHeader("Content-Length", "128");
750         resp2.setHeader("ETag", "\"newetag\"");
751         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
752         resp2.setHeader("Cache-Control", "public, max-age=5");
753 
754         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
755         execute(req1);
756 
757         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
758 
759         final ClassicHttpResponse result = execute(req2);
760 
761         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
762     }
763 
764     @Test
765     public void testReturns304ForIfModifiedSincePassesIfRequestServedFromOrigin() throws Exception {
766         final Instant now = Instant.now();
767         final Instant tenSecondsAgo = now.minusSeconds(10);
768 
769         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
770         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
771 
772         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
773         resp1.setEntity(HttpTestUtils.makeBody(128));
774         resp1.setHeader("Content-Length", "128");
775         resp1.setHeader("ETag", "\"etag\"");
776         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
777         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
778         resp1.setHeader("Cache-Control", "public, max-age=5");
779 
780         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
781         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
782         resp2.setHeader("ETag", "\"etag\"");
783         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
784         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
785         resp2.setHeader("Cache-Control", "public, max-age=5");
786 
787         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
788 
789         execute(req1);
790 
791         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
792 
793         final ClassicHttpResponse result = execute(req2);
794 
795         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
796     }
797 
798     @Test
799     public void testReturns200ForIfModifiedSinceFailsIfRequestServedFromOrigin() throws Exception {
800         final Instant now = Instant.now();
801         final Instant tenSecondsAgo = now.minusSeconds(10);
802 
803         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
804         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
805 
806         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
807         resp1.setEntity(HttpTestUtils.makeBody(128));
808         resp1.setHeader("Content-Length", "128");
809         resp1.setHeader("ETag", "\"etag\"");
810         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
811         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
812         resp1.setHeader("Cache-Control", "public, max-age=5");
813 
814         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
815         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
816         resp2.setEntity(HttpTestUtils.makeBody(128));
817         resp2.setHeader("Content-Length", "128");
818         resp2.setHeader("ETag", "\"newetag\"");
819         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
820         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
821         resp2.setHeader("Cache-Control", "public, max-age=5");
822 
823         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
824 
825         execute(req1);
826 
827         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
828 
829         final ClassicHttpResponse result = execute(req2);
830 
831         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
832     }
833 
834     @Test
835     public void testVariantMissServerIfReturns304CacheReturns200() throws Exception {
836         final Instant now = Instant.now();
837 
838         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
839         req1.addHeader("Accept-Encoding", "gzip");
840 
841         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
842         resp1.setEntity(HttpTestUtils.makeBody(128));
843         resp1.setHeader("Content-Length", "128");
844         resp1.setHeader("Etag", "\"gzip_etag\"");
845         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
846         resp1.setHeader("Vary", "Accept-Encoding");
847         resp1.setHeader("Cache-Control", "public, max-age=3600");
848 
849         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com");
850         req2.addHeader("Accept-Encoding", "deflate");
851 
852         final ClassicHttpRequest req2Server = new HttpGet("http://foo.example.com");
853         req2Server.addHeader("Accept-Encoding", "deflate");
854         req2Server.addHeader("If-None-Match", "\"gzip_etag\"");
855 
856         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
857         resp2.setEntity(HttpTestUtils.makeBody(128));
858         resp2.setHeader("Content-Length", "128");
859         resp2.setHeader("Etag", "\"deflate_etag\"");
860         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
861         resp2.setHeader("Vary", "Accept-Encoding");
862         resp2.setHeader("Cache-Control", "public, max-age=3600");
863 
864         final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
865         req3.addHeader("Accept-Encoding", "gzip,deflate");
866 
867         final ClassicHttpRequest req3Server = new HttpGet("http://foo.example.com");
868         req3Server.addHeader("Accept-Encoding", "gzip,deflate");
869         req3Server.addHeader("If-None-Match", "\"gzip_etag\",\"deflate_etag\"");
870 
871         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
872         resp3.setEntity(HttpTestUtils.makeBody(128));
873         resp3.setHeader("Content-Length", "128");
874         resp3.setHeader("Etag", "\"gzip_etag\"");
875         resp3.setHeader("Date", DateUtils.formatStandardDate(now));
876         resp3.setHeader("Vary", "Accept-Encoding");
877         resp3.setHeader("Cache-Control", "public, max-age=3600");
878 
879         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
880 
881         final ClassicHttpResponse result1 = execute(req1);
882 
883         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
884 
885         final ClassicHttpResponse result2 = execute(req2);
886 
887         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
888 
889         final ClassicHttpResponse result3 = execute(req3);
890 
891         Assertions.assertEquals(HttpStatus.SC_OK, result1.getCode());
892         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
893         Assertions.assertEquals(HttpStatus.SC_OK, result3.getCode());
894     }
895 
896     @Test
897     public void testVariantsMissServerReturns304CacheReturns304() throws Exception {
898         final Instant now = Instant.now();
899 
900         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
901         req1.addHeader("Accept-Encoding", "gzip");
902 
903         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
904         resp1.setEntity(HttpTestUtils.makeBody(128));
905         resp1.setHeader("Content-Length", "128");
906         resp1.setHeader("Etag", "\"gzip_etag\"");
907         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
908         resp1.setHeader("Vary", "Accept-Encoding");
909         resp1.setHeader("Cache-Control", "public, max-age=3600");
910 
911         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com");
912         req2.addHeader("Accept-Encoding", "deflate");
913 
914         final ClassicHttpRequest req2Server = new HttpGet("http://foo.example.com");
915         req2Server.addHeader("Accept-Encoding", "deflate");
916         req2Server.addHeader("If-None-Match", "\"gzip_etag\"");
917 
918         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
919         resp2.setEntity(HttpTestUtils.makeBody(128));
920         resp2.setHeader("Content-Length", "128");
921         resp2.setHeader("Etag", "\"deflate_etag\"");
922         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
923         resp2.setHeader("Vary", "Accept-Encoding");
924         resp2.setHeader("Cache-Control", "public, max-age=3600");
925 
926         final ClassicHttpRequest req4 = new HttpGet("http://foo.example.com");
927         req4.addHeader("Accept-Encoding", "gzip,identity");
928         req4.addHeader("If-None-Match", "\"gzip_etag\"");
929 
930         final ClassicHttpRequest req4Server = new HttpGet("http://foo.example.com");
931         req4Server.addHeader("Accept-Encoding", "gzip,identity");
932         req4Server.addHeader("If-None-Match", "\"gzip_etag\"");
933 
934         final ClassicHttpResponse resp4 = HttpTestUtils.make304Response();
935         resp4.setHeader("Etag", "\"gzip_etag\"");
936         resp4.setHeader("Date", DateUtils.formatStandardDate(now));
937         resp4.setHeader("Vary", "Accept-Encoding");
938         resp4.setHeader("Cache-Control", "public, max-age=3600");
939 
940         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
941 
942         final ClassicHttpResponse result1 = execute(req1);
943 
944         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
945 
946         final ClassicHttpResponse result2 = execute(req2);
947 
948         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp4);
949 
950         final ClassicHttpResponse result4 = execute(req4);
951         Assertions.assertEquals(HttpStatus.SC_OK, result1.getCode());
952         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
953         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result4.getCode());
954 
955     }
956 
957     @Test
958     public void testSocketTimeoutExceptionIsNotSilentlyCatched() throws Exception {
959         final Instant now = Instant.now();
960 
961         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
962 
963         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
964         resp1.setEntity(new InputStreamEntity(new InputStream() {
965             private boolean closed;
966 
967             @Override
968             public void close() throws IOException {
969                 closed = true;
970             }
971 
972             @Override
973             public int read() throws IOException {
974                 if (closed) {
975                     throw new SocketException("Socket closed");
976                 }
977                 throw new SocketTimeoutException("Read timed out");
978             }
979         }, 128, null));
980         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
981 
982         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
983 
984         Assertions.assertThrows(SocketTimeoutException.class, () -> {
985             final ClassicHttpResponse result1 = execute(req1);
986             EntityUtils.toString(result1.getEntity());
987         });
988     }
989 
990     @Test
991     public void testIsSharedCache() {
992         Assertions.assertTrue(config.isSharedCache());
993     }
994 
995     @Test
996     public void testTooLargeResponsesAreNotCached() throws Exception {
997         final HttpHost host = new HttpHost("foo.example.com");
998         final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
999 
1000         final Instant now = Instant.now();
1001         final Instant requestSent = now.plusSeconds(3);
1002         final Instant responseGenerated = now.plusSeconds(2);
1003         final Instant responseReceived = now.plusSeconds(1);
1004 
1005         final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1006         originResponse.setEntity(HttpTestUtils.makeBody(CacheConfig.DEFAULT_MAX_OBJECT_SIZE_BYTES + 1));
1007         originResponse.setHeader("Cache-Control","public, max-age=3600");
1008         originResponse.setHeader("Date", DateUtils.formatStandardDate(responseGenerated));
1009         originResponse.setHeader("ETag", "\"etag\"");
1010 
1011         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
1012         impl.cacheAndReturnResponse(host, request, originResponse, scope, requestSent, responseReceived);
1013 
1014         Mockito.verify(cache, Mockito.never()).createCacheEntry(
1015                 Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
1016     }
1017 
1018     @Test
1019     public void testSmallEnoughResponsesAreCached() throws Exception {
1020         final HttpCache mockCache = Mockito.mock(HttpCache.class);
1021         impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
1022 
1023         final HttpHost host = new HttpHost("foo.example.com");
1024         final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
1025 
1026         final Instant now = Instant.now();
1027         final Instant requestSent = now.plusSeconds(3);
1028         final Instant responseGenerated = now.plusSeconds(2);
1029         final Instant responseReceived = now.plusSeconds(1);
1030 
1031         final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1032         originResponse.setEntity(HttpTestUtils.makeBody(CacheConfig.DEFAULT_MAX_OBJECT_SIZE_BYTES - 1));
1033         originResponse.setHeader("Cache-Control","public, max-age=3600");
1034         originResponse.setHeader("Date", DateUtils.formatStandardDate(responseGenerated));
1035         originResponse.setHeader("ETag", "\"etag\"");
1036 
1037         final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
1038         final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
1039 
1040         Mockito.when(mockCache.createCacheEntry(
1041                 Mockito.eq(host),
1042                 RequestEquivalent.eq(request),
1043                 ResponseEquivalent.eq(response),
1044                 Mockito.any(),
1045                 Mockito.eq(requestSent),
1046                 Mockito.eq(responseReceived))).thenReturn(httpCacheEntry);
1047 
1048         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
1049         impl.cacheAndReturnResponse(host, request, originResponse, scope, requestSent, responseReceived);
1050 
1051         Mockito.verify(mockCache).createCacheEntry(
1052                 Mockito.any(),
1053                 Mockito.any(),
1054                 Mockito.any(),
1055                 Mockito.any(),
1056                 Mockito.any(),
1057                 Mockito.any());
1058     }
1059 
1060     @Test
1061     public void testIfOnlyIfCachedAndNoCacheEntryBackendNotCalled() throws Exception {
1062         request.addHeader("Cache-Control", "only-if-cached");
1063 
1064         final ClassicHttpResponse resp = execute(request);
1065 
1066         Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, resp.getCode());
1067     }
1068 
1069     @Test
1070     public void testSetsRouteInContextOnCacheHit() throws Exception {
1071         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1072         response.setHeader("Cache-Control", "max-age=3600");
1073         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
1074 
1075         final HttpClientContext ctx = HttpClientContext.create();
1076         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1077         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), mockExecChain);
1078         Assertions.assertEquals(route, ctx.getHttpRoute());
1079     }
1080 
1081     @Test
1082     public void testSetsRequestInContextOnCacheHit() throws Exception {
1083         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1084         response.setHeader("Cache-Control", "max-age=3600");
1085         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
1086 
1087         final HttpClientContext ctx = HttpClientContext.create();
1088         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1089         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), mockExecChain);
1090         if (!HttpTestUtils.equivalent(request, ctx.getRequest())) {
1091             assertSame(request, ctx.getRequest());
1092         }
1093     }
1094 
1095     @Test
1096     public void testSetsResponseInContextOnCacheHit() throws Exception {
1097         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1098         response.setHeader("Cache-Control", "max-age=3600");
1099         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
1100 
1101         final HttpClientContext ctx = HttpClientContext.create();
1102         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1103         final ClassicHttpResponse result = impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), null);
1104         if (!HttpTestUtils.equivalent(result, ctx.getResponse())) {
1105             assertSame(result, ctx.getResponse());
1106         }
1107     }
1108 
1109     @Test
1110     public void testSetsRequestSentInContextOnCacheHit() throws Exception {
1111         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1112         response.setHeader("Cache-Control", "max-age=3600");
1113         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
1114 
1115         final HttpClientContext ctx = HttpClientContext.create();
1116         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1117         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), mockExecChain);
1118     }
1119 
1120     @Test
1121     public void testCanCacheAResponseWithoutABody() throws Exception {
1122         final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1123         response.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1124         response.setHeader("Cache-Control", "max-age=300");
1125         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
1126 
1127         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1128         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1129 
1130         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
1131     }
1132 
1133     @Test
1134     public void testNoEntityForIfNoneMatchRequestNotYetInCache() throws Exception {
1135 
1136         final Instant now = Instant.now();
1137         final Instant tenSecondsAgo = now.minusSeconds(10);
1138 
1139         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1140         req1.addHeader("If-None-Match", "\"etag\"");
1141 
1142         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1143         resp1.setHeader("Content-Length", "128");
1144         resp1.setHeader("ETag", "\"etag\"");
1145         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1146         resp1.setHeader("Cache-Control", "public, max-age=5");
1147 
1148         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1149         final ClassicHttpResponse result = execute(req1);
1150 
1151         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
1152         Assertions.assertNull(result.getEntity(), "The 304 response messages MUST NOT contain a message-body");
1153     }
1154 
1155     @Test
1156     public void testNotModifiedResponseUpdatesCacheEntryWhenNoEntity() throws Exception {
1157 
1158         final Instant now = Instant.now();
1159 
1160         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1161         req1.addHeader("If-None-Match", "etag");
1162 
1163         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1164         req2.addHeader("If-None-Match", "etag");
1165 
1166         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1167         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1168         resp1.setHeader("Cache-Control", "max-age=0");
1169         resp1.setHeader("Etag", "etag");
1170 
1171         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
1172         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1173         resp2.setHeader("Cache-Control", "max-age=0");
1174         resp1.setHeader("Etag", "etag");
1175 
1176         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1177 
1178         final ClassicHttpResponse result1 = execute(req1);
1179         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1180 
1181         final ClassicHttpResponse result2 = execute(req2);
1182 
1183         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1184         Assertions.assertEquals("etag", result1.getFirstHeader("Etag").getValue());
1185         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
1186         Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue());
1187     }
1188 
1189     @Test
1190     public void testNotModifiedResponseWithVaryUpdatesCacheEntryWhenNoEntity() throws Exception {
1191 
1192         final Instant now = Instant.now();
1193 
1194         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1195         req1.addHeader("If-None-Match", "etag");
1196 
1197         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1198         req2.addHeader("If-None-Match", "etag");
1199 
1200         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1201         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1202         resp1.setHeader("Cache-Control", "max-age=0");
1203         resp1.setHeader("Etag", "etag");
1204         resp1.setHeader("Vary", "Accept-Encoding");
1205 
1206         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
1207         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1208         resp2.setHeader("Cache-Control", "max-age=0");
1209         resp1.setHeader("Etag", "etag");
1210         resp1.setHeader("Vary", "Accept-Encoding");
1211 
1212         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1213 
1214         final ClassicHttpResponse result1 = execute(req1);
1215 
1216         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1217 
1218         final ClassicHttpResponse result2 = execute(req2);
1219 
1220         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1221         Assertions.assertEquals("etag", result1.getFirstHeader("Etag").getValue());
1222         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
1223         Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue());
1224     }
1225 
1226     @Test
1227     public void testDoesNotSend304ForNonConditionalRequest() throws Exception {
1228 
1229         final Instant now = Instant.now();
1230         final Instant inOneMinute = now.plus(1, ChronoUnit.MINUTES);
1231 
1232         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1233         req1.addHeader("If-None-Match", "etag");
1234 
1235         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1236 
1237         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1238         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1239         resp1.setHeader("Cache-Control", "public, max-age=60");
1240         resp1.setHeader("Expires", DateUtils.formatStandardDate(inOneMinute));
1241         resp1.setHeader("Etag", "etag");
1242         resp1.setHeader("Vary", "Accept-Encoding");
1243 
1244         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK,
1245             "Ok");
1246         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1247         resp2.setHeader("Cache-Control", "public, max-age=60");
1248         resp2.setHeader("Expires", DateUtils.formatStandardDate(inOneMinute));
1249         resp2.setHeader("Etag", "etag");
1250         resp2.setHeader("Vary", "Accept-Encoding");
1251         resp2.setEntity(HttpTestUtils.makeBody(128));
1252 
1253         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1254 
1255         final ClassicHttpResponse result1 = execute(req1);
1256 
1257         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1258 
1259         final ClassicHttpResponse result2 = execute(req2);
1260 
1261         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1262         Assertions.assertNull(result1.getEntity());
1263         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
1264         Assertions.assertNotNull(result2.getEntity());
1265     }
1266 
1267     @Test
1268     public void testUsesVirtualHostForCacheKey() throws Exception {
1269         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1270         response.setHeader("Cache-Control", "max-age=3600");
1271         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(response);
1272 
1273         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1274 
1275         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
1276 
1277         request.setAuthority(new URIAuthority("bar.example.com"));
1278         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1279 
1280         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1281 
1282         impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain);
1283 
1284         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1285     }
1286 
1287 }