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