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.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   * A suite of acceptance tests for compliance with RFC5861, which
62   * describes the stale-if-error and stale-while-revalidate
63   * Cache-Control extensions.
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() throws Exception {
130         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
131         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
132         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
133                 "public, max-age=5, stale-if-error=60");
134 
135         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
136         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
137         final byte[] body101 = HttpTestUtils.makeRandomBytes(101);
138         final ByteArrayInputStream cis = Mockito.spy(new ByteArrayInputStream(body101));
139         final HttpEntity entity = new InputStreamEntity(cis, 101, null);
140         resp2.setEntity(entity);
141 
142         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
143 
144         execute(req1);
145 
146         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
147 
148         execute(req2);
149 
150         Mockito.verify(cis).close();
151     }
152 
153     @Test
154     void testStaleIfErrorInResponseYieldsToMustRevalidate() throws Exception {
155         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
156         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
157         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
158                 "public, max-age=5, stale-if-error=60, must-revalidate");
159 
160         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
161         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
162 
163         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
164 
165         execute(req1);
166 
167         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
168 
169         final ClassicHttpResponse result = execute(req2);
170 
171         assertNotEquals(HttpStatus.SC_OK, result.getCode());
172     }
173 
174     @Test
175     void testStaleIfErrorInResponseYieldsToProxyRevalidateForSharedCache() throws Exception {
176         assertTrue(config.isSharedCache());
177         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
178         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
179         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
180                 "public, max-age=5, stale-if-error=60, proxy-revalidate");
181 
182         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
183         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
184 
185         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
186 
187         execute(req1);
188 
189         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
190 
191         final ClassicHttpResponse result = execute(req2);
192 
193         assertNotEquals(HttpStatus.SC_OK, result.getCode());
194     }
195 
196     @Test
197     void testStaleIfErrorInResponseYieldsToExplicitFreshnessRequest() throws Exception {
198         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
199         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
200         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
201                 "public, max-age=5, stale-if-error=60");
202 
203         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
204         req2.setHeader("Cache-Control","min-fresh=2");
205         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
206 
207         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
208 
209         execute(req1);
210 
211         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
212 
213         final ClassicHttpResponse result = execute(req2);
214 
215         assertNotEquals(HttpStatus.SC_OK, result.getCode());
216     }
217 
218     @Test
219     void testStaleIfErrorInResponseIsFalseReturnsError() throws Exception {
220         final Instant now = Instant.now();
221         final Instant tenSecondsAgo = now.minusSeconds(10);
222         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
223         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
224                 "public, max-age=5, stale-if-error=2");
225 
226         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
227         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
228 
229         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
230 
231         execute(req1);
232 
233         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
234 
235         final ClassicHttpResponse result = execute(req2);
236 
237         assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
238                 result.getCode());
239     }
240 
241     @Test
242     void testStaleIfErrorInRequestIsFalseReturnsError() throws Exception {
243         final Instant now = Instant.now();
244         final Instant tenSecondsAgo = now.minusSeconds(10);
245         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
246         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
247                 "public, max-age=5");
248 
249         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
250         req2.setHeader("Cache-Control","stale-if-error=2");
251         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
252 
253         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
254 
255         execute(req1);
256 
257         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
258 
259         final ClassicHttpResponse result = execute(req2);
260 
261         assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.getCode());
262     }
263 
264 }