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.Iterator;
33 import java.util.Map;
34 import java.util.concurrent.ScheduledExecutorService;
35
36 import org.apache.hc.client5.http.HttpRoute;
37 import org.apache.hc.client5.http.async.methods.SimpleBody;
38 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
39 import org.apache.hc.client5.http.cache.CacheResponseStatus;
40 import org.apache.hc.client5.http.cache.HeaderConstants;
41 import org.apache.hc.client5.http.cache.HttpCacheEntry;
42 import org.apache.hc.client5.http.cache.HttpCacheStorage;
43 import org.apache.hc.client5.http.cache.ResourceFactory;
44 import org.apache.hc.client5.http.cache.ResourceIOException;
45 import org.apache.hc.client5.http.classic.ExecChain;
46 import org.apache.hc.client5.http.classic.ExecChainHandler;
47 import org.apache.hc.client5.http.impl.ExecSupport;
48 import org.apache.hc.client5.http.protocol.HttpClientContext;
49 import org.apache.hc.client5.http.schedule.SchedulingStrategy;
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.protocol.HttpCoreContext;
67 import org.apache.hc.core5.net.URIAuthority;
68 import org.apache.hc.core5.util.Args;
69 import org.apache.hc.core5.util.ByteArrayBuffer;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
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 class CachingExec extends CachingExecBase implements ExecChainHandler {
102
103 private final HttpCache responseCache;
104 private final DefaultCacheRevalidator cacheRevalidator;
105 private final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
106
107 private static final Logger LOG = LoggerFactory.getLogger(CachingExec.class);
108
109 CachingExec(final HttpCache cache, final DefaultCacheRevalidator cacheRevalidator, final CacheConfig config) {
110 super(config);
111 this.responseCache = Args.notNull(cache, "Response cache");
112 this.cacheRevalidator = cacheRevalidator;
113 this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(classicHttpRequest ->
114 ClassicRequestBuilder.copy(classicHttpRequest).build());
115 }
116
117 CachingExec(
118 final HttpCache responseCache,
119 final CacheValidityPolicy validityPolicy,
120 final ResponseCachingPolicy responseCachingPolicy,
121 final CachedHttpResponseGenerator responseGenerator,
122 final CacheableRequestPolicy cacheableRequestPolicy,
123 final CachedResponseSuitabilityChecker suitabilityChecker,
124 final ResponseProtocolCompliance responseCompliance,
125 final RequestProtocolCompliance requestCompliance,
126 final DefaultCacheRevalidator cacheRevalidator,
127 final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder,
128 final CacheConfig config) {
129 super(validityPolicy, responseCachingPolicy, responseGenerator, cacheableRequestPolicy,
130 suitabilityChecker, responseCompliance, requestCompliance, config);
131 this.responseCache = responseCache;
132 this.cacheRevalidator = cacheRevalidator;
133 this.conditionalRequestBuilder = conditionalRequestBuilder;
134 }
135
136 CachingExec(
137 final HttpCache cache,
138 final ScheduledExecutorService executorService,
139 final SchedulingStrategy schedulingStrategy,
140 final CacheConfig config) {
141 this(cache,
142 executorService != null ? new DefaultCacheRevalidator(executorService, schedulingStrategy) : null,
143 config);
144 }
145
146 CachingExec(
147 final ResourceFactory resourceFactory,
148 final HttpCacheStorage storage,
149 final ScheduledExecutorService executorService,
150 final SchedulingStrategy schedulingStrategy,
151 final CacheConfig config) {
152 this(new BasicHttpCache(resourceFactory, storage), executorService, schedulingStrategy, config);
153 }
154
155 @Override
156 public ClassicHttpResponse execute(
157 final ClassicHttpRequest request,
158 final ExecChain.Scope scope,
159 final ExecChain chain) throws IOException, HttpException {
160 Args.notNull(request, "HTTP request");
161 Args.notNull(scope, "Scope");
162
163 final HttpRoute route = scope.route;
164 final HttpClientContext context = scope.clientContext;
165 context.setAttribute(HttpClientContext.HTTP_ROUTE, scope.route);
166 context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
167
168 final URIAuthority authority = request.getAuthority();
169 final String scheme = request.getScheme();
170 final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
171 final String via = generateViaHeader(request);
172
173
174 setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
175
176 if (clientRequestsOurOptions(request)) {
177 setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
178 return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
179 }
180
181 final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context);
182 if (fatalErrorResponse != null) {
183 return convert(fatalErrorResponse, scope);
184 }
185
186 requestCompliance.makeRequestCompliant(request);
187 request.addHeader("Via",via);
188
189 if (!cacheableRequestPolicy.isServableFromCache(request)) {
190 LOG.debug("Request is not servable from cache");
191 responseCache.flushCacheEntriesInvalidatedByRequest(target, request);
192 return callBackend(target, request, scope, chain);
193 }
194
195 final HttpCacheEntry entry = responseCache.getCacheEntry(target, request);
196 if (entry == null) {
197 LOG.debug("Cache miss");
198 return handleCacheMiss(target, request, scope, chain);
199 } else {
200 return handleCacheHit(target, request, scope, chain, entry);
201 }
202 }
203
204 private static ClassicHttpResponse convert(final SimpleHttpResponse cacheResponse, final ExecChain.Scope scope) {
205 if (cacheResponse == null) {
206 return null;
207 }
208 final ClassicHttpResponse response = new BasicClassicHttpResponse(cacheResponse.getCode(), cacheResponse.getReasonPhrase());
209 for (final Iterator<Header> it = cacheResponse.headerIterator(); it.hasNext(); ) {
210 response.addHeader(it.next());
211 }
212 response.setVersion(cacheResponse.getVersion() != null ? cacheResponse.getVersion() : HttpVersion.DEFAULT);
213 final SimpleBody body = cacheResponse.getBody();
214 if (body != null) {
215 final ContentType contentType = body.getContentType();
216 final Header h = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
217 final String contentEncoding = h != null ? h.getValue() : null;
218 if (body.isText()) {
219 response.setEntity(new StringEntity(body.getBodyText(), contentType, contentEncoding, false));
220 } else {
221 response.setEntity(new ByteArrayEntity(body.getBodyBytes(), contentType, contentEncoding, false));
222 }
223 }
224 scope.clientContext.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
225 return response;
226 }
227
228 ClassicHttpResponse callBackend(
229 final HttpHost target,
230 final ClassicHttpRequest request,
231 final ExecChain.Scope scope,
232 final ExecChain chain) throws IOException, HttpException {
233
234 final Instant requestDate = getCurrentDate();
235
236 LOG.debug("Calling the backend");
237 final ClassicHttpResponse backendResponse = chain.proceed(request, scope);
238 try {
239 backendResponse.addHeader("Via", generateViaHeader(backendResponse));
240 return handleBackendResponse(target, request, scope, requestDate, getCurrentDate(), backendResponse);
241 } catch (final IOException | RuntimeException ex) {
242 backendResponse.close();
243 throw ex;
244 }
245 }
246
247 private ClassicHttpResponse handleCacheHit(
248 final HttpHost target,
249 final ClassicHttpRequest request,
250 final ExecChain.Scope scope,
251 final ExecChain chain,
252 final HttpCacheEntry entry) throws IOException, HttpException {
253 final HttpClientContext context = scope.clientContext;
254 context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
255 recordCacheHit(target, request);
256 final Instant now = getCurrentDate();
257 if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
258 LOG.debug("Cache hit");
259 try {
260 return convert(generateCachedResponse(request, context, entry, now), scope);
261 } catch (final ResourceIOException ex) {
262 recordCacheFailure(target, request);
263 if (!mayCallBackend(request)) {
264 return convert(generateGatewayTimeout(context), scope);
265 }
266 setResponseStatus(scope.clientContext, CacheResponseStatus.FAILURE);
267 return chain.proceed(request, scope);
268 }
269 } else if (!mayCallBackend(request)) {
270 LOG.debug("Cache entry not suitable but only-if-cached requested");
271 return convert(generateGatewayTimeout(context), scope);
272 } else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
273 LOG.debug("Revalidating cache entry");
274 try {
275 if (cacheRevalidator != null
276 && !staleResponseNotAllowed(request, entry, now)
277 && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
278 LOG.debug("Serving stale with asynchronous revalidation");
279 final String exchangeId = ExecSupport.getNextExchangeId();
280 context.setExchangeId(exchangeId);
281 final ExecChain.Scope fork = new ExecChain.Scope(
282 exchangeId,
283 scope.route,
284 scope.originalRequest,
285 scope.execRuntime.fork(null),
286 HttpClientContext.create());
287 final SimpleHttpResponse response = generateCachedResponse(request, context, entry, now);
288 cacheRevalidator.revalidateCacheEntry(
289 responseCache.generateKey(target, request, entry),
290 () -> revalidateCacheEntry(target, request, fork, chain, entry));
291 return convert(response, scope);
292 }
293 return revalidateCacheEntry(target, request, scope, chain, entry);
294 } catch (final IOException ioex) {
295 return convert(handleRevalidationFailure(request, context, entry, now), scope);
296 }
297 } else {
298 LOG.debug("Cache entry not usable; calling backend");
299 return callBackend(target, request, scope, chain);
300 }
301 }
302
303 ClassicHttpResponse revalidateCacheEntry(
304 final HttpHost target,
305 final ClassicHttpRequest request,
306 final ExecChain.Scope scope,
307 final ExecChain chain,
308 final HttpCacheEntry cacheEntry) throws IOException, HttpException {
309 Instant requestDate = getCurrentDate();
310 final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
311 scope.originalRequest, cacheEntry);
312
313 ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
314 try {
315 Instant responseDate = getCurrentDate();
316
317 if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
318 backendResponse.close();
319 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
320 scope.originalRequest);
321 requestDate = getCurrentDate();
322 backendResponse = chain.proceed(unconditional, scope);
323 responseDate = getCurrentDate();
324 }
325
326 backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
327
328 final int statusCode = backendResponse.getCode();
329 if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
330 recordCacheUpdate(scope.clientContext);
331 }
332
333 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
334 final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
335 target, request, cacheEntry, backendResponse, requestDate, responseDate);
336 if (suitabilityChecker.isConditional(request)
337 && suitabilityChecker.allConditionalsMatch(request, updatedEntry, Instant.now())) {
338 return convert(responseGenerator.generateNotModifiedResponse(updatedEntry), scope);
339 }
340 return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
341 }
342
343 if (staleIfErrorAppliesTo(statusCode)
344 && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
345 && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
346 try {
347 final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, cacheEntry);
348 cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
349 return convert(cachedResponse, scope);
350 } finally {
351 backendResponse.close();
352 }
353 }
354 return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
355 } catch (final IOException | RuntimeException ex) {
356 backendResponse.close();
357 throw ex;
358 }
359 }
360
361 ClassicHttpResponse handleBackendResponse(
362 final HttpHost target,
363 final ClassicHttpRequest request,
364 final ExecChain.Scope scope,
365 final Instant requestDate,
366 final Instant responseDate,
367 final ClassicHttpResponse backendResponse) throws IOException {
368
369 responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
370
371 responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse);
372 final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
373 if (cacheable) {
374 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
375 return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate);
376 }
377 LOG.debug("Backend response is not cacheable");
378 responseCache.flushCacheEntriesFor(target, request);
379 return backendResponse;
380 }
381
382 ClassicHttpResponse cacheAndReturnResponse(
383 final HttpHost target,
384 final HttpRequest request,
385 final ClassicHttpResponse backendResponse,
386 final ExecChain.Scope scope,
387 final Instant requestSent,
388 final Instant responseReceived) throws IOException {
389 LOG.debug("Caching backend response");
390 final ByteArrayBuffer buf;
391 final HttpEntity entity = backendResponse.getEntity();
392 if (entity != null) {
393 buf = new ByteArrayBuffer(1024);
394 final InputStream inStream = entity.getContent();
395 final byte[] tmp = new byte[2048];
396 long total = 0;
397 int l;
398 while ((l = inStream.read(tmp)) != -1) {
399 buf.append(tmp, 0, l);
400 total += l;
401 if (total > cacheConfig.getMaxObjectSize()) {
402 LOG.debug("Backend response content length exceeds maximum");
403 backendResponse.setEntity(new CombinedEntity(entity, buf));
404 return backendResponse;
405 }
406 }
407 } else {
408 buf = null;
409 }
410 backendResponse.close();
411
412 final HttpCacheEntry cacheEntry;
413 if (cacheConfig.isFreshnessCheckEnabled()) {
414 final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
415 if (DateSupport.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
416 LOG.debug("Backend already contains fresher cache entry");
417 cacheEntry = existingEntry;
418 } else {
419 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
420 LOG.debug("Backend response successfully cached");
421 }
422 } else {
423 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
424 LOG.debug("Backend response successfully cached (freshness check skipped)");
425 }
426 return convert(responseGenerator.generateResponse(request, cacheEntry), scope);
427 }
428
429 private ClassicHttpResponse handleCacheMiss(
430 final HttpHost target,
431 final ClassicHttpRequest request,
432 final ExecChain.Scope scope,
433 final ExecChain chain) throws IOException, HttpException {
434 recordCacheMiss(target, request);
435
436 if (!mayCallBackend(request)) {
437 return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
438 }
439
440 final Map<String, Variant> variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
441 if (variants != null && !variants.isEmpty()) {
442 return negotiateResponseFromVariants(target, request, scope, chain, variants);
443 }
444
445 return callBackend(target, request, scope, chain);
446 }
447
448 ClassicHttpResponse negotiateResponseFromVariants(
449 final HttpHost target,
450 final ClassicHttpRequest request,
451 final ExecChain.Scope scope,
452 final ExecChain chain,
453 final Map<String, Variant> variants) throws IOException, HttpException {
454 final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants);
455
456 final Instant requestDate = getCurrentDate();
457 final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
458 try {
459 final Instant responseDate = getCurrentDate();
460
461 backendResponse.addHeader("Via", generateViaHeader(backendResponse));
462
463 if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
464 return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse);
465 }
466
467 final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
468 if (resultEtagHeader == null) {
469 LOG.warn("304 response did not contain ETag");
470 EntityUtils.consume(backendResponse.getEntity());
471 backendResponse.close();
472 return callBackend(target, request, scope, chain);
473 }
474
475 final String resultEtag = resultEtagHeader.getValue();
476 final Variant matchingVariant = variants.get(resultEtag);
477 if (matchingVariant == null) {
478 LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
479 EntityUtils.consume(backendResponse.getEntity());
480 backendResponse.close();
481 return callBackend(target, request, scope, chain);
482 }
483
484 if (revalidationResponseIsTooOld(backendResponse, matchingVariant.getEntry())
485 && (request.getEntity() == null || request.getEntity().isRepeatable())) {
486 EntityUtils.consume(backendResponse.getEntity());
487 backendResponse.close();
488 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
489 return callBackend(target, unconditional, scope, chain);
490 }
491
492 recordCacheUpdate(scope.clientContext);
493
494 final HttpCacheEntry responseEntry = responseCache.updateVariantCacheEntry(
495 target, conditionalRequest, backendResponse, matchingVariant, requestDate, responseDate);
496 backendResponse.close();
497 if (shouldSendNotModifiedResponse(request, responseEntry)) {
498 return convert(responseGenerator.generateNotModifiedResponse(responseEntry), scope);
499 }
500 final SimpleHttpResponse response = responseGenerator.generateResponse(request, responseEntry);
501 responseCache.reuseVariantEntryFor(target, request, matchingVariant);
502 return convert(response, scope);
503 } catch (final IOException | RuntimeException ex) {
504 backendResponse.close();
505 throw ex;
506 }
507 }
508
509 }