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.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.message.BasicClassicHttpResponse;
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<>(ClassicRequestCopier.INSTANCE);
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(HttpClientContext.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 Date 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 Date 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 final ExecChain.Scope fork = new ExecChain.Scope(
281 exchangeId,
282 scope.route,
283 scope.originalRequest,
284 scope.execRuntime.fork(null),
285 HttpClientContext.create());
286 final SimpleHttpResponse response = generateCachedResponse(request, context, entry, now);
287 cacheRevalidator.revalidateCacheEntry(
288 responseCache.generateKey(target, request, entry),
289 new DefaultCacheRevalidator.RevalidationCall() {
290
291 @Override
292 public ClassicHttpResponse execute() throws HttpException, IOException {
293 return revalidateCacheEntry(target, request, fork, chain, entry);
294 }
295
296 });
297 return convert(response, scope);
298 }
299 return revalidateCacheEntry(target, request, scope, chain, entry);
300 } catch (final IOException ioex) {
301 return convert(handleRevalidationFailure(request, context, entry, now), scope);
302 }
303 } else {
304 LOG.debug("Cache entry not usable; calling backend");
305 return callBackend(target, request, scope, chain);
306 }
307 }
308
309 ClassicHttpResponse revalidateCacheEntry(
310 final HttpHost target,
311 final ClassicHttpRequest request,
312 final ExecChain.Scope scope,
313 final ExecChain chain,
314 final HttpCacheEntry cacheEntry) throws IOException, HttpException {
315 Date requestDate = getCurrentDate();
316 final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
317 scope.originalRequest, cacheEntry);
318
319 ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
320 try {
321 Date responseDate = getCurrentDate();
322
323 if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
324 backendResponse.close();
325 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
326 scope.originalRequest);
327 requestDate = getCurrentDate();
328 backendResponse = chain.proceed(unconditional, scope);
329 responseDate = getCurrentDate();
330 }
331
332 backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
333
334 final int statusCode = backendResponse.getCode();
335 if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
336 recordCacheUpdate(scope.clientContext);
337 }
338
339 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
340 final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
341 target, request, cacheEntry, backendResponse, requestDate, responseDate);
342 if (suitabilityChecker.isConditional(request)
343 && suitabilityChecker.allConditionalsMatch(request, updatedEntry, new Date())) {
344 return convert(responseGenerator.generateNotModifiedResponse(updatedEntry), scope);
345 }
346 return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
347 }
348
349 if (staleIfErrorAppliesTo(statusCode)
350 && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
351 && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
352 try {
353 final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, cacheEntry);
354 cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
355 return convert(cachedResponse, scope);
356 } finally {
357 backendResponse.close();
358 }
359 }
360 return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
361 } catch (final IOException | RuntimeException ex) {
362 backendResponse.close();
363 throw ex;
364 }
365 }
366
367 ClassicHttpResponse handleBackendResponse(
368 final HttpHost target,
369 final ClassicHttpRequest request,
370 final ExecChain.Scope scope,
371 final Date requestDate,
372 final Date responseDate,
373 final ClassicHttpResponse backendResponse) throws IOException {
374
375 responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
376
377 responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse);
378 final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
379 if (cacheable) {
380 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
381 return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate);
382 }
383 LOG.debug("Backend response is not cacheable");
384 responseCache.flushCacheEntriesFor(target, request);
385 return backendResponse;
386 }
387
388 ClassicHttpResponse cacheAndReturnResponse(
389 final HttpHost target,
390 final HttpRequest request,
391 final ClassicHttpResponse backendResponse,
392 final ExecChain.Scope scope,
393 final Date requestSent,
394 final Date responseReceived) throws IOException {
395 LOG.debug("Caching backend response");
396 final ByteArrayBuffer buf;
397 final HttpEntity entity = backendResponse.getEntity();
398 if (entity != null) {
399 buf = new ByteArrayBuffer(1024);
400 final InputStream inStream = entity.getContent();
401 final byte[] tmp = new byte[2048];
402 long total = 0;
403 int l;
404 while ((l = inStream.read(tmp)) != -1) {
405 buf.append(tmp, 0, l);
406 total += l;
407 if (total > cacheConfig.getMaxObjectSize()) {
408 LOG.debug("Backend response content length exceeds maximum");
409 backendResponse.setEntity(new CombinedEntity(entity, buf));
410 return backendResponse;
411 }
412 }
413 } else {
414 buf = null;
415 }
416 backendResponse.close();
417
418 final HttpCacheEntry cacheEntry;
419 if (cacheConfig.isFreshnessCheckEnabled()) {
420 final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
421 if (DateUtils.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
422 LOG.debug("Backend already contains fresher cache entry");
423 cacheEntry = existingEntry;
424 } else {
425 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
426 LOG.debug("Backend response successfully cached");
427 }
428 } else {
429 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
430 LOG.debug("Backend response successfully cached (freshness check skipped)");
431 }
432 return convert(responseGenerator.generateResponse(request, cacheEntry), scope);
433 }
434
435 private ClassicHttpResponse handleCacheMiss(
436 final HttpHost target,
437 final ClassicHttpRequest request,
438 final ExecChain.Scope scope,
439 final ExecChain chain) throws IOException, HttpException {
440 recordCacheMiss(target, request);
441
442 if (!mayCallBackend(request)) {
443 return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
444 }
445
446 final Map<String, Variant> variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
447 if (variants != null && !variants.isEmpty()) {
448 return negotiateResponseFromVariants(target, request, scope, chain, variants);
449 }
450
451 return callBackend(target, request, scope, chain);
452 }
453
454 ClassicHttpResponse negotiateResponseFromVariants(
455 final HttpHost target,
456 final ClassicHttpRequest request,
457 final ExecChain.Scope scope,
458 final ExecChain chain,
459 final Map<String, Variant> variants) throws IOException, HttpException {
460 final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants);
461
462 final Date requestDate = getCurrentDate();
463 final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
464 try {
465 final Date responseDate = getCurrentDate();
466
467 backendResponse.addHeader("Via", generateViaHeader(backendResponse));
468
469 if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
470 return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse);
471 }
472
473 final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
474 if (resultEtagHeader == null) {
475 LOG.warn("304 response did not contain ETag");
476 EntityUtils.consume(backendResponse.getEntity());
477 backendResponse.close();
478 return callBackend(target, request, scope, chain);
479 }
480
481 final String resultEtag = resultEtagHeader.getValue();
482 final Variant matchingVariant = variants.get(resultEtag);
483 if (matchingVariant == null) {
484 LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
485 EntityUtils.consume(backendResponse.getEntity());
486 backendResponse.close();
487 return callBackend(target, request, scope, chain);
488 }
489
490 if (revalidationResponseIsTooOld(backendResponse, matchingVariant.getEntry())
491 && (request.getEntity() == null || request.getEntity().isRepeatable())) {
492 EntityUtils.consume(backendResponse.getEntity());
493 backendResponse.close();
494 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
495 return callBackend(target, unconditional, scope, chain);
496 }
497
498 recordCacheUpdate(scope.clientContext);
499
500 final HttpCacheEntry responseEntry = responseCache.updateVariantCacheEntry(
501 target, conditionalRequest, backendResponse, matchingVariant, requestDate, responseDate);
502 backendResponse.close();
503 if (shouldSendNotModifiedResponse(request, responseEntry)) {
504 return convert(responseGenerator.generateNotModifiedResponse(responseEntry), scope);
505 }
506 final SimpleHttpResponse response = responseGenerator.generateResponse(request, responseEntry);
507 responseCache.reuseVariantEntryFor(target, request, matchingVariant);
508 return convert(response, scope);
509 } catch (final IOException | RuntimeException ex) {
510 backendResponse.close();
511 throw ex;
512 }
513 }
514
515 }