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  import java.io.IOException;
30  import java.io.InputStream;
31  import java.time.Instant;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.concurrent.ScheduledExecutorService;
35  
36  import org.apache.hc.client5.http.HttpRoute;
37  import org.apache.hc.client5.http.async.methods.SimpleBody;
38  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
39  import org.apache.hc.client5.http.cache.CacheResponseStatus;
40  import org.apache.hc.client5.http.cache.HeaderConstants;
41  import org.apache.hc.client5.http.cache.HttpCacheEntry;
42  import org.apache.hc.client5.http.cache.HttpCacheStorage;
43  import org.apache.hc.client5.http.cache.ResourceFactory;
44  import org.apache.hc.client5.http.cache.ResourceIOException;
45  import org.apache.hc.client5.http.classic.ExecChain;
46  import org.apache.hc.client5.http.classic.ExecChainHandler;
47  import org.apache.hc.client5.http.impl.ExecSupport;
48  import org.apache.hc.client5.http.protocol.HttpClientContext;
49  import org.apache.hc.client5.http.schedule.SchedulingStrategy;
50  import org.apache.hc.core5.http.ClassicHttpRequest;
51  import org.apache.hc.core5.http.ClassicHttpResponse;
52  import org.apache.hc.core5.http.ContentType;
53  import org.apache.hc.core5.http.Header;
54  import org.apache.hc.core5.http.HttpEntity;
55  import org.apache.hc.core5.http.HttpException;
56  import org.apache.hc.core5.http.HttpHeaders;
57  import org.apache.hc.core5.http.HttpHost;
58  import org.apache.hc.core5.http.HttpRequest;
59  import org.apache.hc.core5.http.HttpStatus;
60  import org.apache.hc.core5.http.HttpVersion;
61  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
62  import org.apache.hc.core5.http.io.entity.EntityUtils;
63  import org.apache.hc.core5.http.io.entity.StringEntity;
64  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
65  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
66  import org.apache.hc.core5.http.protocol.HttpCoreContext;
67  import org.apache.hc.core5.net.URIAuthority;
68  import org.apache.hc.core5.util.Args;
69  import org.apache.hc.core5.util.ByteArrayBuffer;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  /**
74   * <p>
75   * Request executor in the request execution chain that is responsible for
76   * transparent client-side caching.
77   * </p>
78   * <p>
79   * The current implementation is conditionally
80   * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
81   * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
82   * are obeyed too.
83   * </p>
84   * <p>
85   * Folks that would like to experiment with alternative storage backends
86   * should look at the {@link HttpCacheStorage} interface and the related
87   * package documentation there. You may also be interested in the provided
88   * {@link org.apache.hc.client5.http.impl.cache.ehcache.EhcacheHttpCacheStorage
89   * EhCache} and {@link
90   * org.apache.hc.client5.http.impl.cache.memcached.MemcachedHttpCacheStorage
91   * memcached} storage backends.
92   * </p>
93   * <p>
94   * Further responsibilities such as communication with the opposite
95   * endpoint is delegated to the next executor in the request execution
96   * chain.
97   * </p>
98   *
99   * @since 4.3
100  */
101 class CachingExec extends CachingExecBase implements ExecChainHandler {
102 
103     private final HttpCache responseCache;
104     private final DefaultCacheRevalidator cacheRevalidator;
105     private final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
106 
107     private static final Logger LOG = LoggerFactory.getLogger(CachingExec.class);
108 
109     CachingExec(final HttpCache cache, final DefaultCacheRevalidator cacheRevalidator, final CacheConfig config) {
110         super(config);
111         this.responseCache = Args.notNull(cache, "Response cache");
112         this.cacheRevalidator = cacheRevalidator;
113         this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(classicHttpRequest ->
114                     ClassicRequestBuilder.copy(classicHttpRequest).build());
115     }
116 
117     CachingExec(
118             final HttpCache responseCache,
119             final CacheValidityPolicy validityPolicy,
120             final ResponseCachingPolicy responseCachingPolicy,
121             final CachedHttpResponseGenerator responseGenerator,
122             final CacheableRequestPolicy cacheableRequestPolicy,
123             final CachedResponseSuitabilityChecker suitabilityChecker,
124             final ResponseProtocolCompliance responseCompliance,
125             final RequestProtocolCompliance requestCompliance,
126             final DefaultCacheRevalidator cacheRevalidator,
127             final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder,
128             final CacheConfig config) {
129         super(validityPolicy, responseCachingPolicy, responseGenerator, cacheableRequestPolicy,
130                 suitabilityChecker, responseCompliance, requestCompliance, config);
131         this.responseCache = responseCache;
132         this.cacheRevalidator = cacheRevalidator;
133         this.conditionalRequestBuilder = conditionalRequestBuilder;
134     }
135 
136     CachingExec(
137             final HttpCache cache,
138             final ScheduledExecutorService executorService,
139             final SchedulingStrategy schedulingStrategy,
140             final CacheConfig config) {
141         this(cache,
142                 executorService != null ? new DefaultCacheRevalidator(executorService, schedulingStrategy) : null,
143                 config);
144     }
145 
146     CachingExec(
147             final ResourceFactory resourceFactory,
148             final HttpCacheStorage storage,
149             final ScheduledExecutorService executorService,
150             final SchedulingStrategy schedulingStrategy,
151             final CacheConfig config) {
152         this(new BasicHttpCache(resourceFactory, storage), executorService, schedulingStrategy, config);
153     }
154 
155     @Override
156     public ClassicHttpResponse execute(
157             final ClassicHttpRequest request,
158             final ExecChain.Scope scope,
159             final ExecChain chain) throws IOException, HttpException {
160         Args.notNull(request, "HTTP request");
161         Args.notNull(scope, "Scope");
162 
163         final HttpRoute route = scope.route;
164         final HttpClientContext context = scope.clientContext;
165         context.setAttribute(HttpClientContext.HTTP_ROUTE, scope.route);
166         context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
167 
168         final URIAuthority authority = request.getAuthority();
169         final String scheme = request.getScheme();
170         final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
171         final String via = generateViaHeader(request);
172 
173         // default response context
174         setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
175 
176         if (clientRequestsOurOptions(request)) {
177             setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
178             return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
179         }
180 
181         final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context);
182         if (fatalErrorResponse != null) {
183             return convert(fatalErrorResponse, scope);
184         }
185 
186         requestCompliance.makeRequestCompliant(request);
187         request.addHeader("Via",via);
188 
189         if (!cacheableRequestPolicy.isServableFromCache(request)) {
190             LOG.debug("Request is not servable from cache");
191             responseCache.flushCacheEntriesInvalidatedByRequest(target, request);
192             return callBackend(target, request, scope, chain);
193         }
194 
195         final HttpCacheEntry entry = responseCache.getCacheEntry(target, request);
196         if (entry == null) {
197             LOG.debug("Cache miss");
198             return handleCacheMiss(target, request, scope, chain);
199         } else {
200             return handleCacheHit(target, request, scope, chain, entry);
201         }
202     }
203 
204     private static ClassicHttpResponse convert(final SimpleHttpResponse cacheResponse, final ExecChain.Scope scope) {
205         if (cacheResponse == null) {
206             return null;
207         }
208         final ClassicHttpResponse response = new BasicClassicHttpResponse(cacheResponse.getCode(), cacheResponse.getReasonPhrase());
209         for (final Iterator<Header> it = cacheResponse.headerIterator(); it.hasNext(); ) {
210             response.addHeader(it.next());
211         }
212         response.setVersion(cacheResponse.getVersion() != null ? cacheResponse.getVersion() : HttpVersion.DEFAULT);
213         final SimpleBody body = cacheResponse.getBody();
214         if (body != null) {
215             final ContentType contentType = body.getContentType();
216             final Header h = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
217             final String contentEncoding = h != null ? h.getValue() : null;
218             if (body.isText()) {
219                 response.setEntity(new StringEntity(body.getBodyText(), contentType, contentEncoding, false));
220             } else {
221                 response.setEntity(new ByteArrayEntity(body.getBodyBytes(), contentType, contentEncoding, false));
222             }
223         }
224         scope.clientContext.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
225         return response;
226     }
227 
228     ClassicHttpResponse callBackend(
229             final HttpHost target,
230             final ClassicHttpRequest request,
231             final ExecChain.Scope scope,
232             final ExecChain chain) throws IOException, HttpException  {
233 
234         final Instant requestDate = getCurrentDate();
235 
236         LOG.debug("Calling the backend");
237         final ClassicHttpResponse backendResponse = chain.proceed(request, scope);
238         try {
239             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
240             return handleBackendResponse(target, request, scope, requestDate, getCurrentDate(), backendResponse);
241         } catch (final IOException | RuntimeException ex) {
242             backendResponse.close();
243             throw ex;
244         }
245     }
246 
247     private ClassicHttpResponse handleCacheHit(
248             final HttpHost target,
249             final ClassicHttpRequest request,
250             final ExecChain.Scope scope,
251             final ExecChain chain,
252             final HttpCacheEntry entry) throws IOException, HttpException {
253         final HttpClientContext context  = scope.clientContext;
254         context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
255         recordCacheHit(target, request);
256         final Instant now = getCurrentDate();
257         if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
258             LOG.debug("Cache hit");
259             try {
260                 return convert(generateCachedResponse(request, context, entry, now), scope);
261             } catch (final ResourceIOException ex) {
262                 recordCacheFailure(target, request);
263                 if (!mayCallBackend(request)) {
264                     return convert(generateGatewayTimeout(context), scope);
265                 }
266                 setResponseStatus(scope.clientContext, CacheResponseStatus.FAILURE);
267                 return chain.proceed(request, scope);
268             }
269         } else if (!mayCallBackend(request)) {
270             LOG.debug("Cache entry not suitable but only-if-cached requested");
271             return convert(generateGatewayTimeout(context), scope);
272         } else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
273             LOG.debug("Revalidating cache entry");
274             try {
275                 if (cacheRevalidator != null
276                         && !staleResponseNotAllowed(request, entry, now)
277                         && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
278                     LOG.debug("Serving stale with asynchronous revalidation");
279                     final String exchangeId = ExecSupport.getNextExchangeId();
280                     context.setExchangeId(exchangeId);
281                     final ExecChain.Scope fork = new ExecChain.Scope(
282                             exchangeId,
283                             scope.route,
284                             scope.originalRequest,
285                             scope.execRuntime.fork(null),
286                             HttpClientContext.create());
287                     final SimpleHttpResponse response = generateCachedResponse(request, context, entry, now);
288                     cacheRevalidator.revalidateCacheEntry(
289                             responseCache.generateKey(target, request, entry),
290                             () -> revalidateCacheEntry(target, request, fork, chain, entry));
291                     return convert(response, scope);
292                 }
293                 return revalidateCacheEntry(target, request, scope, chain, entry);
294             } catch (final IOException ioex) {
295                 return convert(handleRevalidationFailure(request, context, entry, now), scope);
296             }
297         } else {
298             LOG.debug("Cache entry not usable; calling backend");
299             return callBackend(target, request, scope, chain);
300         }
301     }
302 
303     ClassicHttpResponse revalidateCacheEntry(
304             final HttpHost target,
305             final ClassicHttpRequest request,
306             final ExecChain.Scope scope,
307             final ExecChain chain,
308             final HttpCacheEntry cacheEntry) throws IOException, HttpException {
309         Instant requestDate = getCurrentDate();
310         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
311                 scope.originalRequest, cacheEntry);
312 
313         ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
314         try {
315             Instant responseDate = getCurrentDate();
316 
317             if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
318                 backendResponse.close();
319                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
320                         scope.originalRequest);
321                 requestDate = getCurrentDate();
322                 backendResponse = chain.proceed(unconditional, scope);
323                 responseDate = getCurrentDate();
324             }
325 
326             backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
327 
328             final int statusCode = backendResponse.getCode();
329             if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
330                 recordCacheUpdate(scope.clientContext);
331             }
332 
333             if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
334                 final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
335                         target, request, cacheEntry, backendResponse, requestDate, responseDate);
336                 if (suitabilityChecker.isConditional(request)
337                         && suitabilityChecker.allConditionalsMatch(request, updatedEntry, Instant.now())) {
338                     return convert(responseGenerator.generateNotModifiedResponse(updatedEntry), scope);
339                 }
340                 return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
341             }
342 
343             if (staleIfErrorAppliesTo(statusCode)
344                     && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
345                     && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
346                 try {
347                     final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, cacheEntry);
348                     cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
349                     return convert(cachedResponse, scope);
350                 } finally {
351                     backendResponse.close();
352                 }
353             }
354             return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
355         } catch (final IOException | RuntimeException ex) {
356             backendResponse.close();
357             throw ex;
358         }
359     }
360 
361     ClassicHttpResponse handleBackendResponse(
362             final HttpHost target,
363             final ClassicHttpRequest request,
364             final ExecChain.Scope scope,
365             final Instant requestDate,
366             final Instant responseDate,
367             final ClassicHttpResponse backendResponse) throws IOException {
368 
369         responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
370 
371         responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse);
372         final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
373         if (cacheable) {
374             storeRequestIfModifiedSinceFor304Response(request, backendResponse);
375             return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate);
376         }
377         LOG.debug("Backend response is not cacheable");
378         responseCache.flushCacheEntriesFor(target, request);
379         return backendResponse;
380     }
381 
382     ClassicHttpResponse cacheAndReturnResponse(
383             final HttpHost target,
384             final HttpRequest request,
385             final ClassicHttpResponse backendResponse,
386             final ExecChain.Scope scope,
387             final Instant requestSent,
388             final Instant responseReceived) throws IOException {
389         LOG.debug("Caching backend response");
390         final ByteArrayBuffer buf;
391         final HttpEntity entity = backendResponse.getEntity();
392         if (entity != null) {
393             buf = new ByteArrayBuffer(1024);
394             final InputStream inStream = entity.getContent();
395             final byte[] tmp = new byte[2048];
396             long total = 0;
397             int l;
398             while ((l = inStream.read(tmp)) != -1) {
399                 buf.append(tmp, 0, l);
400                 total += l;
401                 if (total > cacheConfig.getMaxObjectSize()) {
402                     LOG.debug("Backend response content length exceeds maximum");
403                     backendResponse.setEntity(new CombinedEntity(entity, buf));
404                     return backendResponse;
405                 }
406             }
407         } else {
408             buf = null;
409         }
410         backendResponse.close();
411 
412         final HttpCacheEntry cacheEntry;
413         if (cacheConfig.isFreshnessCheckEnabled()) {
414             final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
415             if (DateSupport.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
416                 LOG.debug("Backend already contains fresher cache entry");
417                 cacheEntry = existingEntry;
418             } else {
419                 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
420                 LOG.debug("Backend response successfully cached");
421             }
422         } else {
423             cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
424             LOG.debug("Backend response successfully cached (freshness check skipped)");
425         }
426         return convert(responseGenerator.generateResponse(request, cacheEntry), scope);
427     }
428 
429     private ClassicHttpResponse handleCacheMiss(
430             final HttpHost target,
431             final ClassicHttpRequest request,
432             final ExecChain.Scope scope,
433             final ExecChain chain) throws IOException, HttpException {
434         recordCacheMiss(target, request);
435 
436         if (!mayCallBackend(request)) {
437             return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
438         }
439 
440         final Map<String, Variant> variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
441         if (variants != null && !variants.isEmpty()) {
442             return negotiateResponseFromVariants(target, request, scope, chain, variants);
443         }
444 
445         return callBackend(target, request, scope, chain);
446     }
447 
448     ClassicHttpResponse negotiateResponseFromVariants(
449             final HttpHost target,
450             final ClassicHttpRequest request,
451             final ExecChain.Scope scope,
452             final ExecChain chain,
453             final Map<String, Variant> variants) throws IOException, HttpException {
454         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants);
455 
456         final Instant requestDate = getCurrentDate();
457         final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
458         try {
459             final Instant responseDate = getCurrentDate();
460 
461             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
462 
463             if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
464                 return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse);
465             }
466 
467             final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
468             if (resultEtagHeader == null) {
469                 LOG.warn("304 response did not contain ETag");
470                 EntityUtils.consume(backendResponse.getEntity());
471                 backendResponse.close();
472                 return callBackend(target, request, scope, chain);
473             }
474 
475             final String resultEtag = resultEtagHeader.getValue();
476             final Variant matchingVariant = variants.get(resultEtag);
477             if (matchingVariant == null) {
478                 LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
479                 EntityUtils.consume(backendResponse.getEntity());
480                 backendResponse.close();
481                 return callBackend(target, request, scope, chain);
482             }
483 
484             if (revalidationResponseIsTooOld(backendResponse, matchingVariant.getEntry())
485                     && (request.getEntity() == null || request.getEntity().isRepeatable())) {
486                 EntityUtils.consume(backendResponse.getEntity());
487                 backendResponse.close();
488                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
489                 return callBackend(target, unconditional, scope, chain);
490             }
491 
492             recordCacheUpdate(scope.clientContext);
493 
494             final HttpCacheEntry responseEntry = responseCache.updateVariantCacheEntry(
495                     target, conditionalRequest, backendResponse, matchingVariant, requestDate, responseDate);
496             backendResponse.close();
497             if (shouldSendNotModifiedResponse(request, responseEntry)) {
498                 return convert(responseGenerator.generateNotModifiedResponse(responseEntry), scope);
499             }
500             final SimpleHttpResponse response = responseGenerator.generateResponse(request, responseEntry);
501             responseCache.reuseVariantEntryFor(target, request, matchingVariant);
502             return convert(response, scope);
503         } catch (final IOException | RuntimeException ex) {
504             backendResponse.close();
505             throw ex;
506         }
507     }
508 
509 }