1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
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
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 }