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