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.ArrayList;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  
38  import org.apache.hc.client5.http.HttpRoute;
39  import org.apache.hc.client5.http.async.methods.SimpleBody;
40  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
41  import org.apache.hc.client5.http.cache.CacheResponseStatus;
42  import org.apache.hc.client5.http.cache.HttpCacheContext;
43  import org.apache.hc.client5.http.cache.HttpCacheEntry;
44  import org.apache.hc.client5.http.cache.HttpCacheStorage;
45  import org.apache.hc.client5.http.cache.ResourceIOException;
46  import org.apache.hc.client5.http.classic.ExecChain;
47  import org.apache.hc.client5.http.classic.ExecChainHandler;
48  import org.apache.hc.client5.http.impl.ExecSupport;
49  import org.apache.hc.client5.http.protocol.HttpClientContext;
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.message.RequestLine;
67  import org.apache.hc.core5.http.protocol.HttpCoreContext;
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.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
135         context.setAttribute(HttpCoreContext.HTTP_RESPONSE, 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 HttpClientContext context = scope.clientContext;
147 
148         if (LOG.isDebugEnabled()) {
149             LOG.debug("{} request via cache: {}", exchangeId, new RequestLine(request));
150         }
151 
152         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MISS);
153 
154         if (clientRequestsOurOptions(request)) {
155             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
156             return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
157         }
158         final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
159         if (LOG.isDebugEnabled()) {
160             LOG.debug("Request cache control: {}", requestCacheControl);
161         }
162         if (!cacheableRequestPolicy.canBeServedFromCache(requestCacheControl, request)) {
163             if (LOG.isDebugEnabled()) {
164                 LOG.debug("{} request cannot be served from cache", exchangeId);
165             }
166             return callBackend(target, request, scope, chain);
167         }
168 
169         final CacheMatch result = responseCache.match(target, request);
170         final CacheHit hit = result != null ? result.hit : null;
171         final CacheHit root = result != null ? result.root : null;
172 
173         if (hit == null) {
174             return handleCacheMiss(requestCacheControl, root, target, request, scope, chain);
175         } else {
176             final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
177             if (LOG.isDebugEnabled()) {
178                 LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
179             }
180             return handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
181         }
182     }
183 
184     private static ClassicHttpResponse convert(final SimpleHttpResponse cacheResponse) {
185         if (cacheResponse == null) {
186             return null;
187         }
188         final ClassicHttpResponse response = new BasicClassicHttpResponse(cacheResponse.getCode(), cacheResponse.getReasonPhrase());
189         for (final Iterator<Header> it = cacheResponse.headerIterator(); it.hasNext(); ) {
190             response.addHeader(it.next());
191         }
192         response.setVersion(cacheResponse.getVersion() != null ? cacheResponse.getVersion() : HttpVersion.DEFAULT);
193         final SimpleBody body = cacheResponse.getBody();
194         if (body != null) {
195             final ContentType contentType = body.getContentType();
196             final Header h = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
197             final String contentEncoding = h != null ? h.getValue() : null;
198             if (body.isText()) {
199                 response.setEntity(new StringEntity(body.getBodyText(), contentType, contentEncoding, false));
200             } else {
201                 response.setEntity(new ByteArrayEntity(body.getBodyBytes(), contentType, contentEncoding, false));
202             }
203         }
204         return response;
205     }
206 
207     ClassicHttpResponse callBackend(
208             final HttpHost target,
209             final ClassicHttpRequest request,
210             final ExecChain.Scope scope,
211             final ExecChain chain) throws IOException, HttpException  {
212 
213         final String exchangeId = scope.exchangeId;
214         final Instant requestDate = getCurrentDate();
215 
216         if (LOG.isDebugEnabled()) {
217             LOG.debug("{} calling the backend", exchangeId);
218         }
219         final ClassicHttpResponse backendResponse = chain.proceed(request, scope);
220         try {
221             return handleBackendResponse(exchangeId, target, request, requestDate, getCurrentDate(), backendResponse);
222         } catch (final IOException | RuntimeException ex) {
223             backendResponse.close();
224             throw ex;
225         }
226     }
227 
228     private ClassicHttpResponse handleCacheHit(
229             final RequestCacheControl requestCacheControl,
230             final ResponseCacheControl responseCacheControl,
231             final CacheHit hit,
232             final HttpHost target,
233             final ClassicHttpRequest request,
234             final ExecChain.Scope scope,
235             final ExecChain chain) throws IOException, HttpException {
236         final String exchangeId = scope.exchangeId;
237         final HttpClientContext context  = scope.clientContext;
238 
239         if (LOG.isDebugEnabled()) {
240             LOG.debug("{} cache hit: {}", exchangeId, new RequestLine(request));
241         }
242 
243         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_HIT);
244         cacheHits.getAndIncrement();
245 
246         final Instant now = getCurrentDate();
247 
248         final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
249         if (LOG.isDebugEnabled()) {
250             LOG.debug("{} cache suitability: {}", exchangeId, cacheSuitability);
251         }
252         if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
253             if (LOG.isDebugEnabled()) {
254                 LOG.debug("{} cache hit is fresh enough", exchangeId);
255             }
256             try {
257                 return convert(generateCachedResponse(request, hit.entry, now));
258             } catch (final ResourceIOException ex) {
259                 if (requestCacheControl.isOnlyIfCached()) {
260                     if (LOG.isDebugEnabled()) {
261                         LOG.debug("{} request marked only-if-cached", exchangeId);
262                     }
263                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
264                     return convert(generateGatewayTimeout());
265                 }
266                 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.FAILURE);
267                 return chain.proceed(request, scope);
268             }
269         } else {
270             if (requestCacheControl.isOnlyIfCached()) {
271                 if (LOG.isDebugEnabled()) {
272                     LOG.debug("{} cache entry not is not fresh and only-if-cached requested", exchangeId);
273                 }
274                 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
275                 return convert(generateGatewayTimeout());
276             } else if (cacheSuitability == CacheSuitability.MISMATCH) {
277                 if (LOG.isDebugEnabled()) {
278                     LOG.debug("{} cache entry does not match the request; calling backend", exchangeId);
279                 }
280                 return callBackend(target, request, scope, chain);
281             } else if (request.getEntity() != null && !request.getEntity().isRepeatable()) {
282                 if (LOG.isDebugEnabled()) {
283                     LOG.debug("{} request is not repeatable; calling backend", exchangeId);
284                 }
285                 return callBackend(target, request, scope, chain);
286             } else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
287                 if (LOG.isDebugEnabled()) {
288                     LOG.debug("{} non-modified cache entry does not match the non-conditional request; calling backend", exchangeId);
289                 }
290                 return callBackend(target, request, scope, chain);
291             } else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
292                 if (LOG.isDebugEnabled()) {
293                     LOG.debug("{} revalidation required; revalidating cache entry", exchangeId);
294                 }
295                 return revalidateCacheEntryWithoutFallback(responseCacheControl, hit, target, request, scope, chain);
296             } else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
297                 if (cacheRevalidator != null) {
298                     if (LOG.isDebugEnabled()) {
299                         LOG.debug("{} serving stale with asynchronous revalidation", exchangeId);
300                     }
301                     final String revalidationExchangeId = ExecSupport.getNextExchangeId();
302                     context.setExchangeId(revalidationExchangeId);
303                     final ExecChain.Scope fork = new ExecChain.Scope(
304                             revalidationExchangeId,
305                             scope.route,
306                             scope.originalRequest,
307                             scope.execRuntime.fork(null),
308                             HttpClientContext.create());
309                     if (LOG.isDebugEnabled()) {
310                         LOG.debug("{} starting asynchronous revalidation exchange {}", exchangeId, revalidationExchangeId);
311                     }
312                     cacheRevalidator.revalidateCacheEntry(
313                             hit.getEntryKey(),
314                             () -> revalidateCacheEntry(responseCacheControl, hit, target, request, fork, chain));
315                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
316                     return convert(unvalidatedCacheHit(request, hit.entry));
317                 } else {
318                     if (LOG.isDebugEnabled()) {
319                         LOG.debug("{} revalidating stale cache entry (asynchronous revalidation disabled)", exchangeId);
320                     }
321                     return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
322                 }
323             } else if (cacheSuitability == CacheSuitability.STALE) {
324                 if (LOG.isDebugEnabled()) {
325                     LOG.debug("{} revalidating stale cache entry", exchangeId);
326                 }
327                 return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
328             } else {
329                 if (LOG.isDebugEnabled()) {
330                     LOG.debug("{} cache entry not usable; calling backend", exchangeId);
331                 }
332                 return callBackend(target, request, scope, chain);
333             }
334         }
335     }
336 
337     ClassicHttpResponse revalidateCacheEntry(
338             final ResponseCacheControl responseCacheControl,
339             final CacheHit hit,
340             final HttpHost target,
341             final ClassicHttpRequest request,
342             final ExecChain.Scope scope,
343             final ExecChain chain) throws IOException, HttpException {
344         final HttpClientContext context = scope.clientContext;
345         Instant requestDate = getCurrentDate();
346         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
347                 responseCacheControl, request, hit.entry);
348 
349         ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
350         try {
351             Instant responseDate = getCurrentDate();
352 
353             if (HttpCacheEntry.isNewer(hit.entry, backendResponse)) {
354                 backendResponse.close();
355                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
356                         scope.originalRequest);
357                 requestDate = getCurrentDate();
358                 backendResponse = chain.proceed(unconditional, scope);
359                 responseDate = getCurrentDate();
360             }
361 
362             final int statusCode = backendResponse.getCode();
363             if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
364                 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.VALIDATED);
365                 cacheUpdates.getAndIncrement();
366             }
367             if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
368                 final CacheHit updated = responseCache.update(hit, target, request, backendResponse, requestDate, responseDate);
369                 return convert(generateCachedResponse(request, updated.entry, responseDate));
370             }
371             return handleBackendResponse(scope.exchangeId, target, conditionalRequest, requestDate, responseDate, backendResponse);
372         } catch (final IOException | RuntimeException ex) {
373             backendResponse.close();
374             throw ex;
375         }
376     }
377 
378     ClassicHttpResponse revalidateCacheEntryWithoutFallback(
379             final ResponseCacheControl responseCacheControl,
380             final CacheHit hit,
381             final HttpHost target,
382             final ClassicHttpRequest request,
383             final ExecChain.Scope scope,
384             final ExecChain chain) throws HttpException {
385         final String exchangeId = scope.exchangeId;
386         final HttpClientContext context = scope.clientContext;
387         try {
388             return revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
389         } catch (final IOException ex) {
390             if (LOG.isDebugEnabled()) {
391                 LOG.debug("{} I/O error while revalidating cache entry", exchangeId, ex);
392             }
393             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
394             return convert(generateGatewayTimeout());
395         }
396     }
397 
398     ClassicHttpResponse revalidateCacheEntryWithFallback(
399             final RequestCacheControl requestCacheControl,
400             final ResponseCacheControl responseCacheControl,
401             final CacheHit hit,
402             final HttpHost target,
403             final ClassicHttpRequest request,
404             final ExecChain.Scope scope,
405             final ExecChain chain) throws HttpException, IOException {
406         final String exchangeId = scope.exchangeId;
407         final HttpClientContext context = scope.clientContext;
408         final ClassicHttpResponse response;
409         try {
410             response = revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
411         } catch (final IOException ex) {
412             if (LOG.isDebugEnabled()) {
413                 LOG.debug("{} I/O error while revalidating cache entry", exchangeId, ex);
414             }
415             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
416             if (suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
417                 if (LOG.isDebugEnabled()) {
418                     LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId);
419                 }
420                 return convert(unvalidatedCacheHit(request, hit.entry));
421             } else {
422                 return convert(generateGatewayTimeout());
423             }
424         }
425         final int status = response.getCode();
426         if (staleIfErrorAppliesTo(status) &&
427                 suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
428             if (LOG.isDebugEnabled()) {
429                 LOG.debug("{} serving stale response due to {} status and stale-if-error enabled", exchangeId, status);
430             }
431             EntityUtils.consume(response.getEntity());
432             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
433             return convert(unvalidatedCacheHit(request, hit.entry));
434         }
435         return response;
436     }
437 
438     ClassicHttpResponse handleBackendResponse(
439             final String exchangeId,
440             final HttpHost target,
441             final ClassicHttpRequest request,
442             final Instant requestDate,
443             final Instant responseDate,
444             final ClassicHttpResponse backendResponse) throws IOException {
445 
446         responseCache.evictInvalidatedEntries(target, request, backendResponse);
447         if (isResponseTooBig(backendResponse.getEntity())) {
448             if (LOG.isDebugEnabled()) {
449                 LOG.debug("{} backend response is known to be too big", exchangeId);
450             }
451             return backendResponse;
452         }
453         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse);
454         final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse);
455         if (cacheable) {
456             storeRequestIfModifiedSinceFor304Response(request, backendResponse);
457             return cacheAndReturnResponse(exchangeId, target, request, backendResponse, requestDate, responseDate);
458         }
459         if (LOG.isDebugEnabled()) {
460             LOG.debug("{} backend response is not cacheable", exchangeId);
461         }
462         return backendResponse;
463     }
464 
465     ClassicHttpResponse cacheAndReturnResponse(
466             final String exchangeId,
467             final HttpHost target,
468             final HttpRequest request,
469             final ClassicHttpResponse backendResponse,
470             final Instant requestSent,
471             final Instant responseReceived) throws IOException {
472         if (LOG.isDebugEnabled()) {
473             LOG.debug("{} caching backend response", exchangeId);
474         }
475 
476         // handle 304 Not Modified responses
477         if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
478             final CacheMatch result = responseCache.match(target ,request);
479             final CacheHit hit = result != null ? result.hit : null;
480             if (hit != null) {
481                 final CacheHit updated = responseCache.update(
482                         hit,
483                         target,
484                         request,
485                         backendResponse,
486                         requestSent,
487                         responseReceived);
488                 return convert(responseGenerator.generateResponse(request, updated.entry));
489             }
490         }
491 
492         final ByteArrayBuffer buf;
493         final HttpEntity entity = backendResponse.getEntity();
494         if (entity != null) {
495             buf = new ByteArrayBuffer(1024);
496             final InputStream inStream = entity.getContent();
497             final byte[] tmp = new byte[2048];
498             long total = 0;
499             int l;
500             while ((l = inStream.read(tmp)) != -1) {
501                 buf.append(tmp, 0, l);
502                 total += l;
503                 if (total > cacheConfig.getMaxObjectSize()) {
504                     if (LOG.isDebugEnabled()) {
505                         LOG.debug("{} backend response content length exceeds maximum", exchangeId);
506                     }
507                     backendResponse.setEntity(new CombinedEntity(entity, buf));
508                     return backendResponse;
509                 }
510             }
511         } else {
512             buf = null;
513         }
514         backendResponse.close();
515 
516         CacheHit hit;
517         if (cacheConfig.isFreshnessCheckEnabled()) {
518             final CacheMatch result = responseCache.match(target ,request);
519             hit = result != null ? result.hit : null;
520             if (HttpCacheEntry.isNewer(hit != null ? hit.entry : null, backendResponse)) {
521                 if (LOG.isDebugEnabled()) {
522                     LOG.debug("{} backend already contains fresher cache entry", exchangeId);
523                 }
524             } else {
525                 hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
526                 if (LOG.isDebugEnabled()) {
527                     LOG.debug("{} backend response successfully cached", exchangeId);
528                 }
529             }
530         } else {
531             hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
532             if (LOG.isDebugEnabled()) {
533                 LOG.debug("{} backend response successfully cached (freshness check skipped)", exchangeId);
534             }
535         }
536         return convert(responseGenerator.generateResponse(request, hit.entry));
537     }
538 
539     private ClassicHttpResponse handleCacheMiss(
540             final RequestCacheControl requestCacheControl,
541             final CacheHit partialMatch,
542             final HttpHost target,
543             final ClassicHttpRequest request,
544             final ExecChain.Scope scope,
545             final ExecChain chain) throws IOException, HttpException {
546         final String exchangeId = scope.exchangeId;
547 
548         if (LOG.isDebugEnabled()) {
549             LOG.debug("{} cache miss: {}", exchangeId, new RequestLine(request));
550         }
551         cacheMisses.getAndIncrement();
552 
553         final HttpClientContext context = scope.clientContext;
554         if (requestCacheControl.isOnlyIfCached()) {
555             if (LOG.isDebugEnabled()) {
556                 LOG.debug("{} request marked only-if-cached", exchangeId);
557             }
558             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
559             return convert(generateGatewayTimeout());
560         }
561         if (partialMatch != null && partialMatch.entry.hasVariants() && request.getEntity() == null) {
562             final List<CacheHit> variants = responseCache.getVariants(partialMatch);
563             if (variants != null && !variants.isEmpty()) {
564                 return negotiateResponseFromVariants(target, request, scope, chain, variants);
565             }
566         }
567 
568         return callBackend(target, request, scope, chain);
569     }
570 
571     ClassicHttpResponse negotiateResponseFromVariants(
572             final HttpHost target,
573             final ClassicHttpRequest request,
574             final ExecChain.Scope scope,
575             final ExecChain chain,
576             final List<CacheHit> variants) throws IOException, HttpException {
577         final String exchangeId = scope.exchangeId;
578 
579         final Map<String, CacheHit> variantMap = new HashMap<>();
580         for (final CacheHit variant : variants) {
581             final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
582             if (header != null) {
583                 variantMap.put(header.getValue(), variant);
584             }
585         }
586 
587         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
588                 request,
589                 new ArrayList<>(variantMap.keySet()));
590 
591         final Instant requestDate = getCurrentDate();
592         final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
593         try {
594             final Instant responseDate = getCurrentDate();
595 
596             if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
597                 return handleBackendResponse(exchangeId, target, request, requestDate, responseDate, backendResponse);
598             } else {
599                 // 304 response are not expected to have an enclosed content body, but still
600                 backendResponse.close();
601             }
602 
603             final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG);
604             if (resultEtagHeader == null) {
605                 if (LOG.isDebugEnabled()) {
606                     LOG.debug("{} 304 response did not contain ETag", exchangeId);
607                 }
608                 return callBackend(target, request, scope, chain);
609             }
610 
611             final String resultEtag = resultEtagHeader.getValue();
612             final CacheHit match = variantMap.get(resultEtag);
613             if (match == null) {
614                 if (LOG.isDebugEnabled()) {
615                     LOG.debug("{} 304 response did not contain ETag matching one sent in If-None-Match", exchangeId);
616                 }
617                 return callBackend(target, request, scope, chain);
618             }
619 
620             if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
621                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
622                 return callBackend(target, unconditional, scope, chain);
623             }
624 
625             final HttpClientContext context = scope.clientContext;
626             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.VALIDATED);
627             cacheUpdates.getAndIncrement();
628 
629             final CacheHit hit = responseCache.storeFromNegotiated(match, target, request, backendResponse, requestDate, responseDate);
630             if (shouldSendNotModifiedResponse(request, hit.entry, responseDate)) {
631                 return convert(responseGenerator.generateNotModifiedResponse(hit.entry));
632             } else {
633                 return convert(responseGenerator.generateResponse(request, hit.entry));
634             }
635         } catch (final IOException | RuntimeException ex) {
636             backendResponse.close();
637             throw ex;
638         }
639     }
640 
641 }