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.assertEquals;
31 import static org.junit.jupiter.api.Assertions.assertNotEquals;
32 import static org.junit.jupiter.api.Assertions.assertTrue;
33
34 import java.io.ByteArrayInputStream;
35 import java.io.IOException;
36 import java.time.Instant;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledThreadPoolExecutor;
39
40 import org.apache.hc.client5.http.HttpRoute;
41 import org.apache.hc.client5.http.cache.HttpCacheContext;
42 import org.apache.hc.client5.http.classic.ExecChain;
43 import org.apache.hc.client5.http.classic.ExecRuntime;
44 import org.apache.hc.core5.http.ClassicHttpRequest;
45 import org.apache.hc.core5.http.ClassicHttpResponse;
46 import org.apache.hc.core5.http.HttpEntity;
47 import org.apache.hc.core5.http.HttpException;
48 import org.apache.hc.core5.http.HttpHost;
49 import org.apache.hc.core5.http.HttpStatus;
50 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
51 import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
52 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
53 import org.junit.jupiter.api.AfterEach;
54 import org.junit.jupiter.api.BeforeEach;
55 import org.junit.jupiter.api.Test;
56 import org.mockito.Mock;
57 import org.mockito.Mockito;
58 import org.mockito.MockitoAnnotations;
59
60
61
62
63
64
65 class TestRFC5861Compliance {
66
67 static final int MAX_BYTES = 1024;
68 static final int MAX_ENTRIES = 100;
69 static final int ENTITY_LENGTH = 128;
70
71 HttpHost host;
72 HttpRoute route;
73 HttpEntity body;
74 HttpCacheContext context;
75 @Mock
76 ExecChain mockExecChain;
77 @Mock
78 ExecRuntime mockExecRuntime;
79 ClassicHttpRequest request;
80 ClassicHttpResponse originResponse;
81 CacheConfig config;
82 CachingExec impl;
83 HttpCache cache;
84 ScheduledExecutorService executorService;
85
86 @BeforeEach
87 void setUp() throws Exception {
88 MockitoAnnotations.openMocks(this);
89
90 host = new HttpHost("foo.example.com", 80);
91
92 route = new HttpRoute(host);
93
94 body = HttpTestUtils.makeBody(ENTITY_LENGTH);
95
96 request = new BasicClassicHttpRequest("GET", "/foo");
97
98 context = HttpCacheContext.create();
99
100 originResponse = HttpTestUtils.make200Response();
101
102 config = CacheConfig.custom()
103 .setMaxCacheEntries(MAX_ENTRIES)
104 .setMaxObjectSize(MAX_BYTES)
105 .build();
106
107 cache = new BasicHttpCache(config);
108 impl = new CachingExec(cache, null, config);
109
110 executorService = new ScheduledThreadPoolExecutor(1);
111
112 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
113 Mockito.when(mockExecRuntime.fork(null)).thenReturn(mockExecRuntime);
114 }
115
116 @AfterEach
117 void cleanup() {
118 executorService.shutdownNow();
119 }
120
121 public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
122 return impl.execute(
123 ClassicRequestBuilder.copy(request).build(),
124 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
125 mockExecChain);
126 }
127
128 @Test
129 void testConsumesErrorResponseWhenServingStale()
130 throws Exception{
131 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
132 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
133 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
134 "public, max-age=5, stale-if-error=60");
135
136 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
137 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
138 final byte[] body101 = HttpTestUtils.makeRandomBytes(101);
139 final ByteArrayInputStream cis = Mockito.spy(new ByteArrayInputStream(body101));
140 final HttpEntity entity = new InputStreamEntity(cis, 101, null);
141 resp2.setEntity(entity);
142
143 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
144
145 execute(req1);
146
147 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
148
149 execute(req2);
150
151 Mockito.verify(cis).close();
152 }
153
154 @Test
155 void testStaleIfErrorInResponseYieldsToMustRevalidate()
156 throws Exception{
157 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
158 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
159 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
160 "public, max-age=5, stale-if-error=60, must-revalidate");
161
162 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
163 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
164
165 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
166
167 execute(req1);
168
169 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
170
171 final ClassicHttpResponse result = execute(req2);
172
173 assertNotEquals(HttpStatus.SC_OK, result.getCode());
174 }
175
176 @Test
177 void testStaleIfErrorInResponseYieldsToProxyRevalidateForSharedCache()
178 throws Exception{
179 assertTrue(config.isSharedCache());
180 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
181 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
182 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
183 "public, max-age=5, stale-if-error=60, proxy-revalidate");
184
185 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
186 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
187
188 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
189
190 execute(req1);
191
192 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
193
194 final ClassicHttpResponse result = execute(req2);
195
196 assertNotEquals(HttpStatus.SC_OK, result.getCode());
197 }
198
199 @Test
200 void testStaleIfErrorInResponseYieldsToExplicitFreshnessRequest()
201 throws Exception{
202 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
203 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
204 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
205 "public, max-age=5, stale-if-error=60");
206
207 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
208 req2.setHeader("Cache-Control","min-fresh=2");
209 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
210
211 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
212
213 execute(req1);
214
215 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
216
217 final ClassicHttpResponse result = execute(req2);
218
219 assertNotEquals(HttpStatus.SC_OK, result.getCode());
220 }
221
222 @Test
223 void testStaleIfErrorInResponseIsFalseReturnsError()
224 throws Exception{
225 final Instant now = Instant.now();
226 final Instant tenSecondsAgo = now.minusSeconds(10);
227 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
228 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
229 "public, max-age=5, stale-if-error=2");
230
231 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
232 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
233
234 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
235
236 execute(req1);
237
238 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
239
240 final ClassicHttpResponse result = execute(req2);
241
242 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
243 result.getCode());
244 }
245
246 @Test
247 void testStaleIfErrorInRequestIsFalseReturnsError()
248 throws Exception{
249 final Instant now = Instant.now();
250 final Instant tenSecondsAgo = now.minusSeconds(10);
251 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
252 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
253 "public, max-age=5");
254
255 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
256 req2.setHeader("Cache-Control","stale-if-error=2");
257 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
258
259 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
260
261 execute(req1);
262
263 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
264
265 final ClassicHttpResponse result = execute(req2);
266
267 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.getCode());
268 }
269
270 }