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.HashMap;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.apache.hc.client5.http.HttpRoute;
38  import org.apache.hc.client5.http.async.methods.SimpleBody;
39  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
40  import org.apache.hc.client5.http.cache.CacheResponseStatus;
41  import org.apache.hc.client5.http.cache.HttpCacheContext;
42  import org.apache.hc.client5.http.cache.HttpCacheEntry;
43  import org.apache.hc.client5.http.cache.HttpCacheStorage;
44  import org.apache.hc.client5.http.cache.RequestCacheControl;
45  import org.apache.hc.client5.http.cache.ResourceIOException;
46  import org.apache.hc.client5.http.cache.ResponseCacheControl;
47  import org.apache.hc.client5.http.classic.ExecChain;
48  import org.apache.hc.client5.http.classic.ExecChainHandler;
49  import org.apache.hc.client5.http.impl.ExecSupport;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.validator.ETag;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.ContentType;
55  import org.apache.hc.core5.http.Header;
56  import org.apache.hc.core5.http.HttpEntity;
57  import org.apache.hc.core5.http.HttpException;
58  import org.apache.hc.core5.http.HttpHeaders;
59  import org.apache.hc.core5.http.HttpHost;
60  import org.apache.hc.core5.http.HttpRequest;
61  import org.apache.hc.core5.http.HttpStatus;
62  import org.apache.hc.core5.http.HttpVersion;
63  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
64  import org.apache.hc.core5.http.io.entity.EntityUtils;
65  import org.apache.hc.core5.http.io.entity.StringEntity;
66  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
67  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
68  import org.apache.hc.core5.net.URIAuthority;
69  import org.apache.hc.core5.util.Args;
70  import org.apache.hc.core5.util.ByteArrayBuffer;
71  import org.slf4j.Logger;
72  import org.slf4j.LoggerFactory;
73  
74  /**
75   * <p>
76   * Request executor in the request execution chain that is responsible for
77   * transparent client-side caching.
78   * </p>
79   * <p>
80   * The current implementation is conditionally
81   * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
82   * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
83   * are obeyed too.
84   * </p>
85   * <p>
86   * Folks that would like to experiment with alternative storage backends
87   * should look at the {@link HttpCacheStorage} interface and the related
88   * package documentation there. You may also be interested in the provided
89   * {@link org.apache.hc.client5.http.impl.cache.ehcache.EhcacheHttpCacheStorage
90   * EhCache} and {@link
91   * org.apache.hc.client5.http.impl.cache.memcached.MemcachedHttpCacheStorage
92   * memcached} storage backends.
93   * </p>
94   * <p>
95   * Further responsibilities such as communication with the opposite
96   * endpoint is delegated to the next executor in the request execution
97   * chain.
98   * </p>
99   *
100  * @since 4.3
101  */
102 class CachingExec extends CachingExecBase implements ExecChainHandler {
103 
104     private final HttpCache responseCache;
105     private final DefaultCacheRevalidator cacheRevalidator;
106     private final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
107 
108     private static final Logger LOG = LoggerFactory.getLogger(CachingExec.class);
109 
110     CachingExec(final HttpCache cache, final DefaultCacheRevalidator cacheRevalidator, final CacheConfig config) {
111         super(config);
112         this.responseCache = Args.notNull(cache, "Response cache");
113         this.cacheRevalidator = cacheRevalidator;
114         this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(classicHttpRequest ->
115                 ClassicRequestBuilder.copy(classicHttpRequest).build());
116     }
117 
118     @Override
119     public ClassicHttpResponse execute(
120             final ClassicHttpRequest request,
121             final ExecChain.Scope scope,
122             final ExecChain chain) throws IOException, HttpException {
123         Args.notNull(request, "HTTP request");
124         Args.notNull(scope, "Scope");
125 
126         final HttpRoute route = scope.route;
127         final HttpClientContext context = scope.clientContext;
128 
129         final URIAuthority authority = request.getAuthority();
130         final String scheme = request.getScheme();
131         final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
132         final ClassicHttpResponse response = doExecute(target, request, scope, chain);
133 
134         context.setRequest(request);
135         context.setResponse(response);
136 
137         return response;
138     }
139 
140     ClassicHttpResponse doExecute(
141             final HttpHost target,
142             final ClassicHttpRequest request,
143             final ExecChain.Scope scope,
144             final ExecChain chain) throws IOException, HttpException {
145         final String exchangeId = scope.exchangeId;
146         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
147 
148         if (LOG.isDebugEnabled()) {
149             LOG.debug("{} request via cache: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
150         }
151 
152         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MISS);
153         context.setCacheEntry(null);
154 
155         if (clientRequestsOurOptions(request)) {
156             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
157             return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
158         }
159 
160         final RequestCacheControl requestCacheControl;
161         if (request.containsHeader(HttpHeaders.CACHE_CONTROL)) {
162             requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
163         } else {
164             requestCacheControl = context.getRequestCacheControlOrDefault();
165             CacheControlHeaderGenerator.INSTANCE.generate(requestCacheControl, request);
166         }
167 
168         if (LOG.isDebugEnabled()) {
169             LOG.debug("Request cache control: {}", requestCacheControl);
170         }
171         if (!cacheableRequestPolicy.canBeServedFromCache(requestCacheControl, request)) {
172             if (LOG.isDebugEnabled()) {
173                 LOG.debug("{} request cannot be served from cache", exchangeId);
174             }
175             return callBackend(target, request, scope, chain);
176         }
177 
178         final CacheMatch result = responseCache.match(target, request);
179         final CacheHit hit = result != null ? result.hit : null;
180         final CacheHit root = result != null ? result.root : null;
181 
182         if (hit == null) {
183             return handleCacheMiss(requestCacheControl, root, target, request, scope, chain);
184         }
185         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
186         context.setResponseCacheControl(responseCacheControl);
187         if (LOG.isDebugEnabled()) {
188             LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
189         }
190         return handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
191     }
192 
193     private static ClassicHttpResponse convert(final SimpleHttpResponse cacheResponse) {
194         if (cacheResponse == null) {
195             return null;
196         }
197         final ClassicHttpResponse response = new BasicClassicHttpResponse(cacheResponse.getCode(), cacheResponse.getReasonPhrase());
198         for (final Iterator<Header> it = cacheResponse.headerIterator(); it.hasNext(); ) {
199             response.addHeader(it.next());
200         }
201         response.setVersion(cacheResponse.getVersion() != null ? cacheResponse.getVersion() : HttpVersion.DEFAULT);
202         final SimpleBody body = cacheResponse.getBody();
203         if (body != null) {
204             final ContentType contentType = body.getContentType();
205             final Header h = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
206             final String contentEncoding = h != null ? h.getValue() : null;
207             if (body.isText()) {
208                 response.setEntity(new StringEntity(body.getBodyText(), contentType, contentEncoding, false));
209             } else {
210                 response.setEntity(new ByteArrayEntity(body.getBodyBytes(), contentType, contentEncoding, false));
211             }
212         }
213         return response;
214     }
215 
216     ClassicHttpResponse callBackend(
217             final HttpHost target,
218             final ClassicHttpRequest request,
219             final ExecChain.Scope scope,
220             final ExecChain chain) throws IOException, HttpException  {
221 
222         final String exchangeId = scope.exchangeId;
223         final Instant requestDate = getCurrentDate();
224 
225         if (LOG.isDebugEnabled()) {
226             LOG.debug("{} calling the backend", exchangeId);
227         }
228         final ClassicHttpResponse backendResponse = chain.proceed(request, scope);
229         try {
230             return handleBackendResponse(target, request, scope, requestDate, getCurrentDate(), backendResponse);
231         } catch (final IOException | RuntimeException ex) {
232             backendResponse.close();
233             throw ex;
234         }
235     }
236 
237     private ClassicHttpResponse handleCacheHit(
238             final RequestCacheControl requestCacheControl,
239             final ResponseCacheControl responseCacheControl,
240             final CacheHit hit,
241             final HttpHost target,
242             final ClassicHttpRequest request,
243             final ExecChain.Scope scope,
244             final ExecChain chain) throws IOException, HttpException {
245         final String exchangeId = scope.exchangeId;
246         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
247 
248         if (LOG.isDebugEnabled()) {
249             LOG.debug("{} cache hit: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
250         }
251 
252         context.setCacheResponseStatus(CacheResponseStatus.CACHE_HIT);
253         cacheHits.getAndIncrement();
254 
255         final Instant now = getCurrentDate();
256 
257         final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
258         if (LOG.isDebugEnabled()) {
259             LOG.debug("{} cache suitability: {}", exchangeId, cacheSuitability);
260         }
261         if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
262             if (LOG.isDebugEnabled()) {
263                 LOG.debug("{} cache hit is fresh enough", exchangeId);
264             }
265             try {
266                 final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now);
267                 context.setCacheEntry(hit.entry);
268                 return convert(cacheResponse);
269             } catch (final ResourceIOException ex) {
270                 if (requestCacheControl.isOnlyIfCached()) {
271                     if (LOG.isDebugEnabled()) {
272                         LOG.debug("{} request marked only-if-cached", exchangeId);
273                     }
274                     context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
275                     return convert(generateGatewayTimeout());
276                 }
277                 context.setCacheResponseStatus(CacheResponseStatus.FAILURE);
278                 return chain.proceed(request, scope);
279             }
280         }
281         if (requestCacheControl.isOnlyIfCached()) {
282             if (LOG.isDebugEnabled()) {
283                 LOG.debug("{} cache entry not is not fresh and only-if-cached requested", exchangeId);
284             }
285             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
286             return convert(generateGatewayTimeout());
287         } else if (cacheSuitability == CacheSuitability.MISMATCH) {
288             if (LOG.isDebugEnabled()) {
289                 LOG.debug("{} cache entry does not match the request; calling backend", exchangeId);
290             }
291             return callBackend(target, request, scope, chain);
292         } else if (request.getEntity() != null && !request.getEntity().isRepeatable()) {
293             if (LOG.isDebugEnabled()) {
294                 LOG.debug("{} request is not repeatable; calling backend", exchangeId);
295             }
296             return callBackend(target, request, scope, chain);
297         } else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
298             if (LOG.isDebugEnabled()) {
299                 LOG.debug("{} non-modified cache entry does not match the non-conditional request; calling backend", exchangeId);
300             }
301             return callBackend(target, request, scope, chain);
302         } else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
303             if (LOG.isDebugEnabled()) {
304                 LOG.debug("{} revalidation required; revalidating cache entry", exchangeId);
305             }
306             return revalidateCacheEntryWithoutFallback(responseCacheControl, hit, target, request, scope, chain);
307         } else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
308             if (cacheRevalidator != null) {
309                 if (LOG.isDebugEnabled()) {
310                     LOG.debug("{} serving stale with asynchronous revalidation", exchangeId);
311                 }
312                 final String revalidationExchangeId = ExecSupport.getNextExchangeId();
313                 context.setExchangeId(revalidationExchangeId);
314                 final ExecChain.Scope fork = new ExecChain.Scope(
315                         revalidationExchangeId,
316                         scope.route,
317                         scope.originalRequest,
318                         scope.execRuntime.fork(null),
319                         HttpCacheContext.create());
320                 if (LOG.isDebugEnabled()) {
321                     LOG.debug("{} starting asynchronous revalidation exchange {}", exchangeId, revalidationExchangeId);
322                 }
323                 cacheRevalidator.revalidateCacheEntry(
324                         hit.getEntryKey(),
325                         () -> revalidateCacheEntry(responseCacheControl, hit, target, request, fork, chain));
326                 context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
327                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
328                 context.setCacheEntry(hit.entry);
329                 return convert(cacheResponse);
330             }
331             if (LOG.isDebugEnabled()) {
332                 LOG.debug("{} revalidating stale cache entry (asynchronous revalidation disabled)", exchangeId);
333             }
334             return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
335         } else if (cacheSuitability == CacheSuitability.STALE) {
336             if (LOG.isDebugEnabled()) {
337                 LOG.debug("{} revalidating stale cache entry", exchangeId);
338             }
339             return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
340         } else {
341             if (LOG.isDebugEnabled()) {
342                 LOG.debug("{} cache entry not usable; calling backend", exchangeId);
343             }
344             return callBackend(target, request, scope, chain);
345         }
346     }
347 
348     ClassicHttpResponse revalidateCacheEntry(
349             final ResponseCacheControl responseCacheControl,
350             final CacheHit hit,
351             final HttpHost target,
352             final ClassicHttpRequest request,
353             final ExecChain.Scope scope,
354             final ExecChain chain) throws IOException, HttpException {
355         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
356         Instant requestDate = getCurrentDate();
357         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
358                 responseCacheControl, request, hit.entry);
359 
360         ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
361         try {
362             Instant responseDate = getCurrentDate();
363 
364             if (HttpCacheEntry.isNewer(hit.entry, backendResponse)) {
365                 backendResponse.close();
366                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
367                         scope.originalRequest);
368                 requestDate = getCurrentDate();
369                 backendResponse = chain.proceed(unconditional, scope);
370                 responseDate = getCurrentDate();
371             }
372 
373             final int statusCode = backendResponse.getCode();
374             if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
375                 context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
376                 cacheUpdates.getAndIncrement();
377             }
378             if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
379                 final CacheHit updated = responseCache.update(hit, target, request, backendResponse, requestDate, responseDate);
380                 final SimpleHttpResponse cacheResponse = generateCachedResponse(request, updated.entry, responseDate);
381                 context.setCacheEntry(updated.entry);
382                 return convert(cacheResponse);
383             }
384             return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
385         } catch (final IOException | RuntimeException ex) {
386             backendResponse.close();
387             throw ex;
388         }
389     }
390 
391     ClassicHttpResponse revalidateCacheEntryWithoutFallback(
392             final ResponseCacheControl responseCacheControl,
393             final CacheHit hit,
394             final HttpHost target,
395             final ClassicHttpRequest request,
396             final ExecChain.Scope scope,
397             final ExecChain chain) throws HttpException {
398         final String exchangeId = scope.exchangeId;
399         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
400         try {
401             return revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
402         } catch (final IOException ex) {
403             if (LOG.isDebugEnabled()) {
404                 LOG.debug("{} I/O error while revalidating cache entry", exchangeId, ex);
405             }
406             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
407             return convert(generateGatewayTimeout());
408         }
409     }
410 
411     ClassicHttpResponse revalidateCacheEntryWithFallback(
412             final RequestCacheControl requestCacheControl,
413             final ResponseCacheControl responseCacheControl,
414             final CacheHit hit,
415             final HttpHost target,
416             final ClassicHttpRequest request,
417             final ExecChain.Scope scope,
418             final ExecChain chain) throws HttpException, IOException {
419         final String exchangeId = scope.exchangeId;
420         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
421         final ClassicHttpResponse response;
422         try {
423             response = revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
424         } catch (final IOException ex) {
425             if (LOG.isDebugEnabled()) {
426                 LOG.debug("{} I/O error while revalidating cache entry", exchangeId, ex);
427             }
428             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
429             if (suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
430                 if (LOG.isDebugEnabled()) {
431                     LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId);
432                 }
433                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
434                 context.setCacheEntry(hit.entry);
435                 return convert(cacheResponse);
436             }
437             return convert(generateGatewayTimeout());
438         }
439         final int status = response.getCode();
440         if (staleIfErrorAppliesTo(status) &&
441                 suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
442             if (LOG.isDebugEnabled()) {
443                 LOG.debug("{} serving stale response due to {} status and stale-if-error enabled", exchangeId, status);
444             }
445             EntityUtils.consume(response.getEntity());
446             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
447             final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
448             context.setCacheEntry(hit.entry);
449             return convert(cacheResponse);
450         }
451         return response;
452     }
453 
454     ClassicHttpResponse handleBackendResponse(
455             final HttpHost target,
456             final ClassicHttpRequest request,
457             final ExecChain.Scope scope,
458             final Instant requestDate,
459             final Instant responseDate,
460             final ClassicHttpResponse backendResponse) throws IOException {
461         final String exchangeId = scope.exchangeId;
462         responseCache.evictInvalidatedEntries(target, request, backendResponse);
463         if (isResponseTooBig(backendResponse.getEntity())) {
464             if (LOG.isDebugEnabled()) {
465                 LOG.debug("{} backend response is known to be too big", exchangeId);
466             }
467             return backendResponse;
468         }
469         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
470         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse);
471         context.setResponseCacheControl(responseCacheControl);
472         final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse);
473         if (cacheable) {
474             storeRequestIfModifiedSinceFor304Response(request, backendResponse);
475             if (LOG.isDebugEnabled()) {
476                 LOG.debug("{} caching backend response", exchangeId);
477             }
478             return cacheAndReturnResponse(target, request, scope, backendResponse, requestDate, responseDate);
479         }
480         if (LOG.isDebugEnabled()) {
481             LOG.debug("{} backend response is not cacheable", exchangeId);
482         }
483         return backendResponse;
484     }
485 
486     ClassicHttpResponse cacheAndReturnResponse(
487             final HttpHost target,
488             final HttpRequest request,
489             final ExecChain.Scope scope,
490             final ClassicHttpResponse backendResponse,
491             final Instant requestSent,
492             final Instant responseReceived) throws IOException {
493         final String exchangeId = scope.exchangeId;
494         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
495         final int statusCode = backendResponse.getCode();
496         // handle 304 Not Modified responses
497         if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
498             final CacheMatch result = responseCache.match(target ,request);
499             final CacheHit hit = result != null ? result.hit : null;
500             if (hit != null) {
501                 final CacheHit updated = responseCache.update(
502                         hit,
503                         target,
504                         request,
505                         backendResponse,
506                         requestSent,
507                         responseReceived);
508                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry);
509                 context.setCacheEntry(hit.entry);
510                 return convert(cacheResponse);
511             }
512         }
513 
514         final ByteArrayBuffer buf;
515         final HttpEntity entity = backendResponse.getEntity();
516         if (entity != null) {
517             buf = new ByteArrayBuffer(1024);
518             final InputStream inStream = entity.getContent();
519             final byte[] tmp = new byte[2048];
520             long total = 0;
521             int l;
522             while ((l = inStream.read(tmp)) != -1) {
523                 buf.append(tmp, 0, l);
524                 total += l;
525                 if (total > cacheConfig.getMaxObjectSize()) {
526                     if (LOG.isDebugEnabled()) {
527                         LOG.debug("{} backend response content length exceeds maximum", exchangeId);
528                     }
529                     backendResponse.setEntity(new CombinedEntity(entity, buf));
530                     return backendResponse;
531                 }
532             }
533         } else {
534             buf = null;
535         }
536         backendResponse.close();
537 
538         CacheHit hit;
539         if (cacheConfig.isFreshnessCheckEnabled() && statusCode != HttpStatus.SC_NOT_MODIFIED) {
540             final CacheMatch result = responseCache.match(target ,request);
541             hit = result != null ? result.hit : null;
542             if (HttpCacheEntry.isNewer(hit != null ? hit.entry : null, backendResponse)) {
543                 if (LOG.isDebugEnabled()) {
544                     LOG.debug("{} backend already contains fresher cache entry", exchangeId);
545                 }
546             } else {
547                 hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
548                 if (LOG.isDebugEnabled()) {
549                     LOG.debug("{} backend response successfully cached", exchangeId);
550                 }
551             }
552         } else {
553             hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
554             if (LOG.isDebugEnabled()) {
555                 LOG.debug("{} backend response successfully cached (freshness check skipped)", exchangeId);
556             }
557         }
558         final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
559         context.setCacheEntry(hit.entry);
560         return convert(cacheResponse);
561     }
562 
563     private ClassicHttpResponse handleCacheMiss(
564             final RequestCacheControl requestCacheControl,
565             final CacheHit partialMatch,
566             final HttpHost target,
567             final ClassicHttpRequest request,
568             final ExecChain.Scope scope,
569             final ExecChain chain) throws IOException, HttpException {
570         final String exchangeId = scope.exchangeId;
571 
572         if (LOG.isDebugEnabled()) {
573             LOG.debug("{} cache miss: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
574         }
575         cacheMisses.getAndIncrement();
576 
577         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
578         if (requestCacheControl.isOnlyIfCached()) {
579             if (LOG.isDebugEnabled()) {
580                 LOG.debug("{} request marked only-if-cached", exchangeId);
581             }
582             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
583             return convert(generateGatewayTimeout());
584         }
585         if (partialMatch != null && partialMatch.entry.hasVariants() && request.getEntity() == null) {
586             final List<CacheHit> variants = responseCache.getVariants(partialMatch);
587             if (variants != null && !variants.isEmpty()) {
588                 return negotiateResponseFromVariants(target, request, scope, chain, variants);
589             }
590         }
591 
592         return callBackend(target, request, scope, chain);
593     }
594 
595     ClassicHttpResponse negotiateResponseFromVariants(
596             final HttpHost target,
597             final ClassicHttpRequest request,
598             final ExecChain.Scope scope,
599             final ExecChain chain,
600             final List<CacheHit> variants) throws IOException, HttpException {
601         final String exchangeId = scope.exchangeId;
602 
603         final Map<ETag, CacheHit> variantMap = new HashMap<>();
604         for (final CacheHit variant : variants) {
605             final ETag eTag = variant.entry.getETag();
606             if (eTag != null) {
607                 variantMap.put(eTag, variant);
608             }
609         }
610 
611         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
612                 request,
613                 variantMap.keySet());
614 
615         final Instant requestDate = getCurrentDate();
616         final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
617         try {
618             final Instant responseDate = getCurrentDate();
619 
620             if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
621                 return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse);
622             }
623             // 304 response are not expected to have an enclosed content body, but still
624             backendResponse.close();
625 
626             final ETag resultEtag = ETag.get(backendResponse);
627             if (resultEtag == null) {
628                 if (LOG.isDebugEnabled()) {
629                     LOG.debug("{} 304 response did not contain ETag", exchangeId);
630                 }
631                 return callBackend(target, request, scope, chain);
632             }
633 
634             final CacheHit match = variantMap.get(resultEtag);
635             if (match == null) {
636                 if (LOG.isDebugEnabled()) {
637                     LOG.debug("{} 304 response did not contain ETag matching one sent in If-None-Match", exchangeId);
638                 }
639                 return callBackend(target, request, scope, chain);
640             }
641 
642             if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
643                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
644                 return callBackend(target, unconditional, scope, chain);
645             }
646 
647             final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
648             context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
649             cacheUpdates.getAndIncrement();
650 
651             final CacheHit hit = responseCache.storeFromNegotiated(match, target, request, backendResponse, requestDate, responseDate);
652             final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, responseDate);
653             context.setCacheEntry(hit.entry);
654             return convert(cacheResponse);
655         } catch (final IOException | RuntimeException ex) {
656             backendResponse.close();
657             throw ex;
658         }
659     }
660 
661 }