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.assertTrue;
32  
33  import java.io.ByteArrayInputStream;
34  import java.io.IOException;
35  import java.time.Instant;
36  import java.util.concurrent.ScheduledExecutorService;
37  import java.util.concurrent.ScheduledThreadPoolExecutor;
38  
39  import org.apache.hc.client5.http.HttpRoute;
40  import org.apache.hc.client5.http.cache.HttpCacheContext;
41  import org.apache.hc.client5.http.classic.ExecChain;
42  import org.apache.hc.client5.http.classic.ExecRuntime;
43  import org.apache.hc.core5.http.ClassicHttpRequest;
44  import org.apache.hc.core5.http.ClassicHttpResponse;
45  import org.apache.hc.core5.http.HttpEntity;
46  import org.apache.hc.core5.http.HttpException;
47  import org.apache.hc.core5.http.HttpHost;
48  import org.apache.hc.core5.http.HttpStatus;
49  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
50  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
51  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
52  import org.junit.jupiter.api.AfterEach;
53  import org.junit.jupiter.api.BeforeEach;
54  import org.junit.jupiter.api.Test;
55  import org.mockito.Mock;
56  import org.mockito.Mockito;
57  import org.mockito.MockitoAnnotations;
58  
59  /**
60   * A suite of acceptance tests for compliance with RFC5861, which
61   * describes the stale-if-error and stale-while-revalidate
62   * Cache-Control extensions.
63   */
64  public class TestRFC5861Compliance {
65  
66      static final int MAX_BYTES = 1024;
67      static final int MAX_ENTRIES = 100;
68      static final int ENTITY_LENGTH = 128;
69  
70      HttpHost host;
71      HttpRoute route;
72      HttpEntity body;
73      HttpCacheContext context;
74      @Mock
75      ExecChain mockExecChain;
76      @Mock
77      ExecRuntime mockExecRuntime;
78      ClassicHttpRequest request;
79      ClassicHttpResponse originResponse;
80      CacheConfig config;
81      CachingExec impl;
82      HttpCache cache;
83      ScheduledExecutorService executorService;
84  
85      @BeforeEach
86      public void setUp() throws Exception {
87          MockitoAnnotations.openMocks(this);
88  
89          host = new HttpHost("foo.example.com", 80);
90  
91          route = new HttpRoute(host);
92  
93          body = HttpTestUtils.makeBody(ENTITY_LENGTH);
94  
95          request = new BasicClassicHttpRequest("GET", "/foo");
96  
97          context = HttpCacheContext.create();
98  
99          originResponse = HttpTestUtils.make200Response();
100 
101         config = CacheConfig.custom()
102                 .setMaxCacheEntries(MAX_ENTRIES)
103                 .setMaxObjectSize(MAX_BYTES)
104                 .build();
105 
106         cache = new BasicHttpCache(config);
107         impl = new CachingExec(cache, null, config);
108 
109         executorService = new ScheduledThreadPoolExecutor(1);
110 
111         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
112         Mockito.when(mockExecRuntime.fork(null)).thenReturn(mockExecRuntime);
113     }
114 
115     @AfterEach
116     public void cleanup() {
117         executorService.shutdownNow();
118     }
119 
120     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
121         return impl.execute(
122                 ClassicRequestBuilder.copy(request).build(),
123                 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
124                 mockExecChain);
125     }
126 
127     @Test
128     public void testConsumesErrorResponseWhenServingStale()
129             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 buf = new ByteArrayInputStream(body101);
139         final ConsumableInputStream cis = new ConsumableInputStream(buf);
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         assertTrue(cis.wasClosed());
152     }
153 
154     @Test
155     public 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         assertTrue(HttpStatus.SC_OK != result.getCode());
174     }
175 
176     @Test
177     public 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         assertTrue(HttpStatus.SC_OK != result.getCode());
197     }
198 
199     @Test
200     public 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         assertTrue(HttpStatus.SC_OK != result.getCode());
220     }
221 
222     @Test
223     public 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     public 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 }