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.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
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
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
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
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 }