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