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