blob: 0219f3311bc33773e15ab77b4046d2cc6011e2e0 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.RequestCacheControl;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.apache.hc.core5.util.TimeValue;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class TestCachedResponseSuitabilityChecker {
private Instant now;
private Instant elevenSecondsAgo;
private Instant tenSecondsAgo;
private Instant nineSecondsAgo;
private HttpRequest request;
private HttpCacheEntry entry;
private RequestCacheControl requestCacheControl;
private ResponseCacheControl responseCacheControl;
private CachedResponseSuitabilityChecker impl;
@BeforeEach
public void setUp() {
now = Instant.now();
elevenSecondsAgo = now.minusSeconds(11);
tenSecondsAgo = now.minusSeconds(10);
nineSecondsAgo = now.minusSeconds(9);
request = new BasicHttpRequest("GET", "/foo");
entry = HttpTestUtils.makeCacheEntry();
requestCacheControl = RequestCacheControl.builder().build();
responseCacheControl = ResponseCacheControl.builder().build();
impl = new CachedResponseSuitabilityChecker(CacheConfig.DEFAULT);
}
private HttpCacheEntry makeEntry(final Instant requestDate,
final Instant responseDate,
final Method method,
final String requestUri,
final Header[] requestHeaders,
final int status,
final Header[] responseHeaders) {
return HttpTestUtils.makeCacheEntry(requestDate, responseDate, method, requestUri, requestHeaders,
status, responseHeaders, HttpTestUtils.makeNullResource());
}
private HttpCacheEntry makeEntry(final Header... headers) {
return makeEntry(elevenSecondsAgo, nineSecondsAgo, Method.GET, "/foo", null, 200, headers);
}
private HttpCacheEntry makeEntry(final Instant requestDate,
final Instant responseDate,
final Header... headers) {
return makeEntry(requestDate, responseDate, Method.GET, "/foo", null, 200, headers);
}
private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header... headers) {
return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, null, 200, headers);
}
private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header[] requestHeaders,
final int status, final Header[] responseHeaders) {
return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, requestHeaders,
status, responseHeaders);
}
@Test
public void testRequestMethodMatch() {
request = new BasicHttpRequest("GET", "/foo");
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
request = new BasicHttpRequest("HEAD", "/foo");
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
request = new BasicHttpRequest("POST", "/foo");
Assertions.assertFalse(impl.requestMethodMatch(request, entry));
request = new BasicHttpRequest("HEAD", "/foo");
entry = makeEntry(Method.HEAD, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
request = new BasicHttpRequest("GET", "/foo");
entry = makeEntry(Method.HEAD, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertFalse(impl.requestMethodMatch(request, entry));
}
@Test
public void testRequestUriMatch() {
request = new BasicHttpRequest("GET", "/foo");
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertTrue(impl.requestUriMatch(request, entry));
request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo");
entry = makeEntry(Method.GET, "http://some-host:80/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertTrue(impl.requestUriMatch(request, entry));
request = new BasicHttpRequest("GET", new HttpHost("Some-Host"), "/foo?bar");
entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertTrue(impl.requestUriMatch(request, entry));
request = new BasicHttpRequest("GET", new HttpHost("some-other-host"), "/foo");
entry = makeEntry(Method.GET, "http://some-host:80/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertFalse(impl.requestUriMatch(request, entry));
request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo?huh");
entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertFalse(impl.requestUriMatch(request, entry));
}
@Test
public void testRequestHeadersMatch() {
request = BasicRequestBuilder.get("/foo").build();
entry = makeEntry(
Method.GET, "/foo",
new Header[]{},
200,
new Header[]{
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
});
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
request = BasicRequestBuilder.get("/foo").build();
entry = makeEntry(
Method.GET, "/foo",
new Header[]{},
200,
new Header[]{
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("Vary", "")
});
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
request = BasicRequestBuilder.get("/foo")
.addHeader("Accept-Encoding", "blah")
.build();
entry = makeEntry(
Method.GET, "/foo",
new Header[]{
new BasicHeader("Accept-Encoding", "blah")
},
200,
new Header[]{
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("Vary", "Accept-Encoding")
});
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
request = BasicRequestBuilder.get("/foo")
.addHeader("Accept-Encoding", "gzip, deflate, deflate , zip, ")
.build();
entry = makeEntry(
Method.GET, "/foo",
new Header[]{
new BasicHeader("Accept-Encoding", " gzip, zip, deflate")
},
200,
new Header[]{
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("Vary", "Accept-Encoding")
});
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
request = BasicRequestBuilder.get("/foo")
.addHeader("Accept-Encoding", "gzip, deflate, zip")
.build();
entry = makeEntry(
Method.GET, "/foo",
new Header[]{
new BasicHeader("Accept-Encoding", " gzip, deflate")
},
200,
new Header[]{
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("Vary", "Accept-Encoding")
});
Assertions.assertFalse(impl.requestHeadersMatch(request, entry));
}
@Test
public void testResponseNoCache() {
entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(false)
.build();
Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(true)
.build();
Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(true)
.setNoCacheFields("stuff", "more-stuff")
.build();
Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("stuff", "booh"));
Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
}
@Test
public void testSuitableIfCacheEntryIsFresh() {
entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testNotSuitableIfCacheEntryIsNotFresh() {
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
Assertions.assertEquals(CacheSuitability.STALE, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testNotSuitableIfRequestHasNoCache() {
requestCacheControl = RequestCacheControl.builder()
.setNoCache(true)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testNotSuitableIfAgeExceedsRequestMaxAge() {
requestCacheControl = RequestCacheControl.builder()
.setMaxAge(10)
.build();
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfFreshAndAgeIsUnderRequestMaxAge() {
requestCacheControl = RequestCacheControl.builder()
.setMaxAge(15)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfFreshAndFreshnessLifetimeGreaterThanRequestMinFresh() {
requestCacheControl = RequestCacheControl.builder()
.setMinFresh(10)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testNotSuitableIfFreshnessLifetimeLessThanRequestMinFresh() {
requestCacheControl = RequestCacheControl.builder()
.setMinFresh(10)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(15)
.build();
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableEvenIfStaleButPermittedByRequestMaxStale() {
requestCacheControl = RequestCacheControl.builder()
.setMaxStale(10)
.build();
final Header[] headers = {
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
};
entry = makeEntry(headers);
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
Assertions.assertEquals(CacheSuitability.FRESH_ENOUGH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testNotSuitableIfStaleButTooStaleForRequestMaxStale() {
requestCacheControl = RequestCacheControl.builder()
.setMaxStale(2)
.build();
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfCacheEntryIsHeuristicallyFreshEnough() {
final Instant oneSecondAgo = now.minusSeconds(1);
final Instant twentyOneSecondsAgo = now.minusSeconds(21);
entry = makeEntry(oneSecondAgo, oneSecondAgo,
new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(twentyOneSecondsAgo)));
final CacheConfig config = CacheConfig.custom()
.setHeuristicCachingEnabled(true)
.setHeuristicCoefficient(0.1f).build();
impl = new CachedResponseSuitabilityChecker(config);
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfCacheEntryIsHeuristicallyFreshEnoughByDefault() {
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final CacheConfig config = CacheConfig.custom()
.setHeuristicCachingEnabled(true)
.setHeuristicDefaultLifetime(TimeValue.ofSeconds(20L))
.build();
impl = new CachedResponseSuitabilityChecker(config);
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfRequestMethodisHEAD() {
final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
entry = makeEntry(
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
}
@Test
public void testSuitableForGETIfEntryDoesNotSpecifyARequestMethodButContainsEntity() {
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableForGETIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethodButContains204Response() {
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableForHEADIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethod() {
final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
final Header[] headers = {
};
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
}
@Test
public void testNotSuitableIfGetRequestWithHeadCacheEntry() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.HEAD, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(3600)
.build();
// Validate that the cache entry is not suitable for the GET request
Assertions.assertEquals(CacheSuitability.MISMATCH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfErrorRequestCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
// the entry has been stale for 6 seconds
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.build();
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.setMinFresh(4) // should take precedence over stale-if-error
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(-1) // not set or not valid
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorResponseCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(10)
.build();
// the entry has been stale for 6 seconds
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(-1) // not set or not valid
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorRequestCacheControlTakesPrecedenceOverResponseCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(5)
.build();
// the entry has been stale for 6 seconds
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.build();
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorConfigDefault() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom()
.setStaleIfErrorEnabled(true)
.build());
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
}