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