HTTPCLIENT-2277: deprecation of obsolete config parameters and removal of oudated or meaningless tests (#484)

* HTTPCLIENT-2277: Deprecated 303 response caching switch as no longer required by RFC 9111

* Javadoc improvements (no functional changes)

* HTTPCLIENT-2277: Revision of HTTP cache protocol requirement and recommendation test cases:
* Removed links to RFC 2616
* Removed verbatim quotes from RFC 2616
* Removed obsolete test cases and test cases without result verification / assertions
* Removed test cases unrelated to HTTP caching
* Removed test cases without test result assertions
This commit is contained in:
Oleg Kalnichevski 2023-09-21 19:28:37 +02:00
parent c7d79190e6
commit 1b0e9cf7b0
10 changed files with 186 additions and 2733 deletions

View File

@ -30,21 +30,18 @@ import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
/**
* <p>Java Beans-style configuration for caching {@link org.apache.hc.client5.http.classic.HttpClient}.
* Any class in the caching module that has configuration options should take a
* {@link CacheConfig} argument in one of its constructors. A
* {@code CacheConfig} instance has sane and conservative defaults, so the
* easiest way to specify options is to get an instance and then set just
* the options you want to modify from their defaults.</p>
*
* <p><b>N.B.</b> This class is only for caching-specific configuration; to
* configure the behavior of the rest of the client, configure the
* {@link org.apache.hc.client5.http.classic.HttpClient} used as the &quot;backend&quot;
* for the {@code CachingHttpClient}.</p>
* <p>Configuration for HTTP caches</p>
*
* <p>Cache configuration can be grouped into the following categories:</p>
*
* <p><b>Cache size.</b> If the backend storage supports these limits, you
* <p><b>Protocol options.</b> I some cases the HTTP protocol allows for
* conditional behaviors or optional protocol extensions. Such conditional
* protocol behaviors or extensions can be turned on or off here.
* See {@link CacheConfig#isNeverCacheHTTP10ResponsesWithQuery()},
* {@link CacheConfig#isNeverCacheHTTP11ResponsesWithQuery()},
* {@link CacheConfig#isStaleIfErrorEnabled()}</p>
*
* <p><b>Cache size.</b> If the backend storage supports these limits, one
* can specify the {@link CacheConfig#getMaxCacheEntries maximum number of
* cache entries} as well as the {@link CacheConfig#getMaxObjectSize()}
* maximum cacheable response body size}.</p>
@ -54,46 +51,25 @@ import org.apache.hc.core5.util.TimeValue;
* responses to requests with {@code Authorization} headers or responses
* marked with {@code Cache-Control: private}. If, however, the cache
* is only going to be used by one logical "user" (behaving similarly to a
* browser cache), then you will want to {@link
* CacheConfig#isSharedCache()} turn off the shared cache setting}.</p>
* browser cache), then one may want to {@link CacheConfig#isSharedCache()}
* turn off the shared cache setting}.</p>
*
* <p><b>303 caching</b>. RFC2616 explicitly disallows caching 303 responses;
* however, the HTTPbis working group says they can be cached
* if explicitly indicated in the response headers and permitted by the request method.
* (They also indicate that disallowing 303 caching is actually an unintended
* spec error in RFC2616).
* This behavior is off by default, to err on the side of a conservative
* adherence to the existing standard, but you may want to
* {@link Builder#setAllow303Caching(boolean) enable it}.
*
* <p><b>Weak ETags on PUT/DELETE If-Match requests</b>. RFC2616 explicitly
* prohibits the use of weak validators in non-GET requests, however, the
* HTTPbis working group says while the limitation for weak validators on ranged
* requests makes sense, weak ETag validation is useful on full non-GET
* requests; e.g., PUT with If-Match. This behavior is off by default, to err on
* the side of a conservative adherence to the existing standard, but you may
* want to {@link Builder#setWeakETagOnPutDeleteAllowed(boolean) enable it}.
*
* <p><b>Heuristic caching</b>. Per RFC2616, a cache may cache certain cache
* entries even if no explicit cache control headers are set by the origin.
* This behavior is off by default, but you may want to turn this on if you
* are working with an origin that doesn't set proper headers but where you
* still want to cache the responses. You will want to {@link
* CacheConfig#isHeuristicCachingEnabled()} enable heuristic caching},
* <p><b>Heuristic caching</b>. Per HTTP caching specification, a cache may
* cache certain cache entries even if no explicit cache control headers are
* set by the origin. This behavior is off by default, but you may want to
* turn this on if you are working with an origin that doesn't set proper
* headers but where one may still want to cache the responses. Use {@link
* CacheConfig#isHeuristicCachingEnabled()} to enable heuristic caching},
* then specify either a {@link CacheConfig#getHeuristicDefaultLifetime()
* default freshness lifetime} and/or a {@link
* CacheConfig#getHeuristicCoefficient() fraction of the time since
* the resource was last modified}. See Sections
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.2">
* 13.2.2</a> and <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4">
* 13.2.4</a> of the HTTP/1.1 RFC for more details on heuristic caching.</p>
* the resource was last modified}.
*
* <p><b>Background validation</b>. The cache module supports the
* {@code stale-while-revalidate} directive of
* <a href="http://tools.ietf.org/html/rfc5861">RFC5861</a>, which allows
* certain cache entry revalidations to happen in the background. Asynchronous
* validation is enabled by default but it could be disabled by setting the number
* of re-validation workers to {@code 0} with {@link CacheConfig#getAsynchronousWorkers()}
* {@code stale-while-revalidate} directive, which allows certain cache entry
* revalidations to happen in the background. Asynchronous validation is enabled
* by default but it could be disabled by setting the number of re-validation
* workers to {@code 0} with {@link CacheConfig#getAsynchronousWorkers()}
* parameter</p>
*/
public class CacheConfig implements Cloneable {
@ -113,8 +89,10 @@ public class CacheConfig implements Cloneable {
*/
public final static int DEFAULT_MAX_UPDATE_RETRIES = 1;
/** Default setting for 303 caching
/**
* @deprecated No longer applicable. Do not use.
*/
@Deprecated
public final static boolean DEFAULT_303_CACHING_ENABLED = false;
/**
@ -147,7 +125,6 @@ public class CacheConfig implements Cloneable {
private final long maxObjectSize;
private final int maxCacheEntries;
private final int maxUpdateRetries;
private final boolean allow303Caching;
private final boolean heuristicCachingEnabled;
private final float heuristicCoefficient;
private final TimeValue heuristicDefaultLifetime;
@ -168,7 +145,6 @@ public class CacheConfig implements Cloneable {
final long maxObjectSize,
final int maxCacheEntries,
final int maxUpdateRetries,
final boolean allow303Caching,
final boolean heuristicCachingEnabled,
final float heuristicCoefficient,
final TimeValue heuristicDefaultLifetime,
@ -182,7 +158,6 @@ public class CacheConfig implements Cloneable {
this.maxObjectSize = maxObjectSize;
this.maxCacheEntries = maxCacheEntries;
this.maxUpdateRetries = maxUpdateRetries;
this.allow303Caching = allow303Caching;
this.heuristicCachingEnabled = heuristicCachingEnabled;
this.heuristicCoefficient = heuristicCoefficient;
this.heuristicDefaultLifetime = heuristicDefaultLifetime;
@ -257,11 +232,11 @@ public class CacheConfig implements Cloneable {
}
/**
* Returns whether 303 caching is enabled.
* @return {@code true} if it is enabled.
* @deprecated No longer applicable. Do not use.
*/
@Deprecated
public boolean is303CachingEnabled() {
return allow303Caching;
return true;
}
/**
@ -356,7 +331,6 @@ public class CacheConfig implements Cloneable {
private long maxObjectSize;
private int maxCacheEntries;
private int maxUpdateRetries;
private boolean allow303Caching;
private boolean heuristicCachingEnabled;
private float heuristicCoefficient;
private TimeValue heuristicDefaultLifetime;
@ -371,7 +345,6 @@ public class CacheConfig implements Cloneable {
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
this.heuristicCachingEnabled = DEFAULT_HEURISTIC_CACHING_ENABLED;
this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
@ -407,12 +380,10 @@ public class CacheConfig implements Cloneable {
}
/**
* Enables or disables 303 caching.
* @param allow303Caching should be {@code true} to
* permit 303 caching, {@code false} to disable it.
* @deprecated Has no effect. Do not use.
*/
@Deprecated
public Builder setAllow303Caching(final boolean allow303Caching) {
this.allow303Caching = allow303Caching;
return this;
}
@ -537,7 +508,6 @@ public class CacheConfig implements Cloneable {
maxObjectSize,
maxCacheEntries,
maxUpdateRetries,
allow303Caching,
heuristicCachingEnabled,
heuristicCoefficient,
heuristicDefaultLifetime,
@ -557,7 +527,6 @@ public class CacheConfig implements Cloneable {
builder.append("[maxObjectSize=").append(this.maxObjectSize)
.append(", maxCacheEntries=").append(this.maxCacheEntries)
.append(", maxUpdateRetries=").append(this.maxUpdateRetries)
.append(", 303CachingEnabled=").append(this.allow303Caching)
.append(", heuristicCachingEnabled=").append(this.heuristicCachingEnabled)
.append(", heuristicCoefficient=").append(this.heuristicCoefficient)
.append(", heuristicDefaultLifetime=").append(this.heuristicDefaultLifetime)

View File

@ -100,9 +100,8 @@ class CachedHttpResponseGenerator {
final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
// The response MUST include the following headers
// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
// - Date, unless its omission is required by section 14.8.1
// - Date
Header dateHeader = entry.getFirstHeader(HttpHeaders.DATE);
if (dateHeader == null) {
dateHeader = new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));

View File

@ -56,8 +56,6 @@ import org.slf4j.LoggerFactory;
public class CachingExecBase {
final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
final AtomicLong cacheHits = new AtomicLong();
final AtomicLong cacheMisses = new AtomicLong();
final AtomicLong cacheUpdates = new AtomicLong();
@ -98,7 +96,6 @@ public class CachingExecBase {
this.cacheConfig.getMaxObjectSize(),
this.cacheConfig.isSharedCache(),
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
this.cacheConfig.is303CachingEnabled(),
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery(),
this.cacheConfig.isStaleIfErrorEnabled());
}
@ -268,16 +265,6 @@ public class CachingExecBase {
}
}
/**
* Reports whether this {@code CachingHttpClient} implementation
* supports byte-range requests as specified by the {@code Range}
* and {@code Content-Range} headers.
* @return {@code true} if byte-range requests are supported
*/
boolean supportsRangeAndContentRangeHeaders() {
return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
}
Instant getCurrentDate() {
return Instant.now();
}
@ -299,7 +286,7 @@ public class CachingExecBase {
// either backend response or cached entry did not have a valid
// Date header, so we can't tell if they are out of order
// according to the origin clock; thus we can skip the
// unconditional retry recommended in 13.2.6 of RFC 2616.
// unconditional retry.
return DateSupport.isBefore(backendResponse, cacheEntry, HttpHeaders.DATE);
}

View File

@ -29,9 +29,6 @@ package org.apache.hc.client5.http.impl.cache;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@ -71,20 +68,12 @@ class ResponseCachingPolicy {
*/
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME;
private final static Set<Integer> CACHEABLE_STATUS_CODES =
new HashSet<>(Arrays.asList(HttpStatus.SC_OK,
HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
HttpStatus.SC_MULTIPLE_CHOICES,
HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_GONE));
private static final Logger LOG = LoggerFactory.getLogger(ResponseCachingPolicy.class);
private final long maxObjectSizeBytes;
private final boolean sharedCache;
private final boolean neverCache1_0ResponsesWithQueryString;
private final boolean neverCache1_1ResponsesWithQueryString;
private final Set<Integer> uncacheableStatusCodes;
/**
* A flag indicating whether serving stale cache entries is allowed when an error occurs
@ -94,32 +83,6 @@ class ResponseCachingPolicy {
*/
private final boolean staleIfErrorEnabled;
/**
* Define a cache policy that limits the size of things that should be stored
* in the cache to a maximum of {@link HttpResponse} bytes in size.
*
* @param maxObjectSizeBytes the size to limit items into the cache
* @param sharedCache whether to behave as a shared cache (true) or a
* non-shared/private cache (false)
* @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
* to cache if explicit cache headers are found.
* @param allow303Caching if this policy is permitted to cache 303 response
* @param neverCache1_1ResponsesWithQueryString {@code true} to never cache HTTP 1.1 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
*/
public ResponseCachingPolicy(final long maxObjectSizeBytes,
final boolean sharedCache,
final boolean neverCache1_0ResponsesWithQueryString,
final boolean allow303Caching,
final boolean neverCache1_1ResponsesWithQueryString) {
this(maxObjectSizeBytes,
sharedCache,
neverCache1_0ResponsesWithQueryString,
allow303Caching,
neverCache1_1ResponsesWithQueryString,
false);
}
/**
* Constructs a new ResponseCachingPolicy with the specified cache policy settings and stale-if-error support.
*
@ -129,8 +92,6 @@ class ResponseCachingPolicy {
* non-shared/private cache (false)
* @param neverCache1_0ResponsesWithQueryString {@code true} to never cache HTTP 1.0 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
* @param allow303Caching {@code true} if this policy is permitted to cache 303 responses,
* {@code false} otherwise
* @param neverCache1_1ResponsesWithQueryString {@code true} to never cache HTTP 1.1 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
* @param staleIfErrorEnabled {@code true} to enable the stale-if-error cache directive, which
@ -141,18 +102,12 @@ class ResponseCachingPolicy {
public ResponseCachingPolicy(final long maxObjectSizeBytes,
final boolean sharedCache,
final boolean neverCache1_0ResponsesWithQueryString,
final boolean allow303Caching,
final boolean neverCache1_1ResponsesWithQueryString,
final boolean staleIfErrorEnabled) {
this.maxObjectSizeBytes = maxObjectSizeBytes;
this.sharedCache = sharedCache;
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
this.neverCache1_1ResponsesWithQueryString = neverCache1_1ResponsesWithQueryString;
if (allow303Caching) {
uncacheableStatusCodes = new HashSet<>(Collections.singletonList(HttpStatus.SC_PARTIAL_CONTENT));
} else {
uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
}
this.staleIfErrorEnabled = staleIfErrorEnabled;
}
@ -172,15 +127,15 @@ class ResponseCachingPolicy {
}
final int status = response.getCode();
if (CACHEABLE_STATUS_CODES.contains(status)) {
if (isKnownCacheableStatusCode(status)) {
// these response codes MAY be cached
cacheable = true;
} else if (uncacheableStatusCodes.contains(status)) {
} else if (isKnownNonCacheableStatusCode(status)) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} response is not cacheable", status);
}
return false;
} else if (unknownStatusCode(status)) {
} else if (isUnknownStatusCode(status)) {
// a response with an unknown status code MUST NOT be
// cached
if (LOG.isDebugEnabled()) {
@ -247,7 +202,19 @@ class ResponseCachingPolicy {
return cacheable || isExplicitlyCacheable(cacheControl, response);
}
private boolean unknownStatusCode(final int status) {
private static boolean isKnownCacheableStatusCode(final int status) {
return status == HttpStatus.SC_OK ||
status == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION ||
status == HttpStatus.SC_MULTIPLE_CHOICES ||
status == HttpStatus.SC_MOVED_PERMANENTLY ||
status == HttpStatus.SC_GONE;
}
private static boolean isKnownNonCacheableStatusCode(final int status) {
return status == HttpStatus.SC_PARTIAL_CONTENT;
}
private static boolean isUnknownStatusCode(final int status) {
if (status >= 100 && status <= 101) {
return false;
}
@ -507,7 +474,6 @@ class ResponseCachingPolicy {
}
/**
* This method checks if a given HTTP status code is understood according to RFC 7231.
* Understood status codes include:
* - All 2xx (Successful) status codes (200-299)
* - All 3xx (Redirection) status codes (300-399)

View File

@ -30,6 +30,7 @@ import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
@ -61,24 +62,6 @@ import org.junit.jupiter.api.Assertions;
public class HttpTestUtils {
private static final String[] SINGLE_HEADERS = { "Accept-Ranges", "Age", "Authorization",
"Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type",
"Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since",
"If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location",
"Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
"User-Agent", "Vary" };
/*
* Determines whether a given header name may only appear once in a message.
*/
public static boolean isSingleHeader(final String name) {
for (final String s : SINGLE_HEADERS) {
if (s.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/*
* Assertions.asserts that two request or response bodies are byte-equivalent.
*/
@ -103,24 +86,28 @@ public class HttpTestUtils {
/*
* Retrieves the full header value by combining multiple headers and
* separating with commas, canonicalizing whitespace along the way.
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
*/
public static String getCanonicalHeaderValue(final HttpMessage r, final String name) {
if (isSingleHeader(name)) {
final int n = r.countHeaders(name);
r.getFirstHeader(name);
if (n == 0) {
return null;
} else if (n == 1) {
final Header h = r.getFirstHeader(name);
return (h != null) ? h.getValue() : null;
}
final StringBuilder buf = new StringBuilder();
boolean first = true;
for (final Header h : r.getHeaders(name)) {
if (!first) {
buf.append(", ");
return h != null ? h.getValue() : null;
} else {
final StringBuilder buf = new StringBuilder();
for (final Iterator<Header> it = r.headerIterator(name); it.hasNext(); ) {
if (buf.length() > 0) {
buf.append(", ");
}
final Header header = it.next();
if (header != null) {
buf.append(header.getValue().trim());
}
}
buf.append(h.getValue().trim());
first = false;
return buf.toString();
}
return buf.toString();
}
/*
@ -132,7 +119,7 @@ public class HttpTestUtils {
if (!HttpCacheEntryFactory.isHopByHop(h)) {
final String r1val = getCanonicalHeaderValue(r1, h.getName());
final String r2val = getCanonicalHeaderValue(r2, h.getName());
if (!r1val.equals(r2val)) {
if (!Objects.equals(r1val, r2val)) {
return false;
}
}
@ -146,21 +133,23 @@ public class HttpTestUtils {
* is semantically transparent, the client receives exactly the same
* response (except for hop-by-hop headers) that it would have received had
* its request been handled directly by the origin server."
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html#sec1.3
*/
public static boolean semanticallyTransparent(
final ClassicHttpResponse r1, final ClassicHttpResponse r2) throws Exception {
final boolean entitiesEquivalent = equivalent(r1.getEntity(), r2.getEntity());
if (!entitiesEquivalent) {
return false;
}
final boolean statusLinesEquivalent = Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
final boolean statusLineEquivalent = Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
&& r1.getCode() == r2.getCode();
if (!statusLinesEquivalent) {
if (!statusLineEquivalent) {
return false;
}
return isEndToEndHeaderSubset(r1, r2);
final boolean headerEquivalent = isEndToEndHeaderSubset(r1, r2);
if (!headerEquivalent) {
return false;
}
final boolean entityEquivalent = equivalent(r1.getEntity(), r2.getEntity());
if (!entityEquivalent) {
return false;
}
return true;
}
/* Assertions.asserts that protocol versions equivalent. */

View File

@ -102,20 +102,6 @@ public class TestCacheKeyGenerator {
new BasicHttpRequest("GET", "/full_episodes")));
}
/*
* "When comparing two URIs to decide if they match or not, a client
* SHOULD use a case-sensitive octet-by-octet comparison of the entire
* URIs, with these exceptions:
* - A port that is empty or not given is equivalent to the default
* port for that URI-reference;
* - Comparisons of host names MUST be case-insensitive;
* - Comparisons of scheme names MUST be case-insensitive;
* - An empty abs_path is equivalent to an abs_path of "/".
* Characters other than those in the 'reserved' and 'unsafe' sets
* (see RFC 2396 [42]) are equivalent to their '"%" HEX HEX' encoding."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2.3
*/
@Test
public void testEmptyPortEquivalentToDefaultPortForHttp() {
final HttpHost host1 = new HttpHost("foo.example.com:");

View File

@ -1,185 +0,0 @@
/*
* ====================================================================
* 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.io.IOException;
import java.time.Instant;
import java.util.Random;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.cache.HttpCacheContext;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
* We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
* of the rules for proxies apply to us, as far as proper operation of the
* requests that pass through us. Generally speaking, we want to make sure that
* any response returned from our HttpClient.execute() methods is conditionally
* compliant with the rules for an HTTP/1.1 server, and that any requests we
* pass downstream to the backend HttpClient are are conditionally compliant
* with the rules for an HTTP/1.1 client.
*
* There are some cases where strictly behaving as a compliant caching proxy
* would result in strange behavior, since we're attached as part of a client
* and are expected to be a drop-in replacement. The test cases captured here
* document the places where we differ from the HTTP RFC.
*/
@SuppressWarnings("boxing") // test code
public class TestProtocolDeviations {
private static final int MAX_BYTES = 1024;
private static final int MAX_ENTRIES = 100;
HttpHost host;
HttpRoute route;
@Mock
ExecRuntime mockEndpoint;
@Mock
ExecChain mockExecChain;
ClassicHttpRequest request;
HttpCacheContext context;
ClassicHttpResponse originResponse;
ExecChainHandler impl;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
host = new HttpHost("foo.example.com", 80);
route = new HttpRoute(host);
request = new BasicClassicHttpRequest("GET", "/foo");
context = HttpCacheContext.create();
originResponse = make200Response();
final CacheConfig config = CacheConfig.custom()
.setMaxCacheEntries(MAX_ENTRIES)
.setMaxObjectSize(MAX_BYTES)
.build();
final HttpCache cache = new BasicHttpCache(config);
impl = createCachingExecChain(cache, config);
}
private ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
return impl.execute(request,
new ExecChain.Scope("test", route, request, mockEndpoint, context),
mockExecChain);
}
protected ExecChainHandler createCachingExecChain(final HttpCache cache, final CacheConfig config) {
return new CachingExec(cache, null, config);
}
private ClassicHttpResponse make200Response() {
final ClassicHttpResponse out = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
out.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
out.setHeader("Server", "MockOrigin/1.0");
out.setEntity(makeBody(128));
return out;
}
private HttpEntity makeBody(final int nbytes) {
final byte[] bytes = new byte[nbytes];
new Random().nextBytes(bytes);
return new ByteArrayEntity(bytes, null);
}
/*
* "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
* header field (section 14.47) containing a challenge applicable to the
* requested resource."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
*/
@Test
public void testPassesOnOrigin401ResponseWithoutWWWAuthenticateHeader() throws Exception {
originResponse = new BasicClassicHttpResponse(401, "Unauthorized");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final HttpResponse result = execute(request);
Assertions.assertSame(originResponse, result);
}
/*
* "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
* header containing a list of valid methods for the requested resource.
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
*/
@Test
public void testPassesOnOrigin405WithoutAllowHeader() throws Exception {
originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final HttpResponse result = execute(request);
Assertions.assertSame(originResponse, result);
}
/*
* "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
* Proxy-Authenticate header field (section 14.33) containing a challenge
* applicable to the proxy for the requested resource."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
*/
@Test
public void testPassesOnOrigin407WithoutAProxyAuthenticateHeader() throws Exception {
originResponse = new BasicClassicHttpResponse(407, "Proxy Authentication Required");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final HttpResponse result = execute(request);
Assertions.assertSame(originResponse, result);
}
}

View File

@ -26,7 +26,6 @@
*/
package org.apache.hc.client5.http.impl.cache;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -36,14 +35,11 @@ import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.List;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
@ -55,7 +51,6 @@ import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@ -70,8 +65,8 @@ import org.mockito.MockitoAnnotations;
/*
* This test class captures functionality required to achieve unconditional
* compliance with the HTTP/1.1 spec, i.e. all the SHOULD, SHOULD NOT,
* RECOMMENDED, and NOT RECOMMENDED behaviors.
* compliance with the HTTP/1.1 caching protocol (SHOULD, SHOULD NOT,
* RECOMMENDED, and NOT RECOMMENDED behaviors).
*/
public class TestProtocolRecommendations {
@ -133,13 +128,6 @@ public class TestProtocolRecommendations {
mockExecChain);
}
/*
* "304 Not Modified. ... If the conditional GET used a strong cache
* validator (see section 13.3.3), the response SHOULD NOT include
* other entity-headers."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
*/
private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
final String headerName, final String headerValue, final String validatorHeader,
final String validator, final String conditionalHeader) throws Exception {
@ -158,10 +146,8 @@ public class TestProtocolRecommendations {
execute(req1);
final ClassicHttpResponse result = execute(req2);
if (HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
assertFalse(result.containsHeader(headerName));
}
assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
assertFalse(result.containsHeader(headerName));
}
private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
@ -237,55 +223,6 @@ public class TestProtocolRecommendations {
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
final String validatorHeader, final String validator, final String conditionalHeader) throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
req1.setHeader("Range","bytes=0-127");
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader(validatorHeader, validator);
resp1.setHeader("Content-Range", "bytes 0-127/256");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("If-Range", validator);
req2.setHeader("Range","bytes=0-127");
req2.setHeader(conditionalHeader, validator);
try (final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified")) {
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader(validatorHeader, validator);
}
// cache module does not currently deal with byte ranges, but we want
// this test to work even if it does some day
execute(req1);
final ClassicHttpResponse result = execute(req2);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
if (allRequests.isEmpty() && HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
// cache generated a 304
assertFalse(result.containsHeader("Content-Range"));
}
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange() throws Exception {
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
"ETag", "\"etag\"", "If-None-Match");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange() throws Exception {
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
"Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo), "If-Modified-Since");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType() throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
@ -310,11 +247,6 @@ public class TestProtocolRecommendations {
"Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
}
/*
* "For this reason, a cache SHOULD NOT return a stale response if the
* client explicitly requests a first-hand or fresh one, unless it is
* impossible to comply for technical or policy reasons."
*/
private ClassicHttpRequest requestToPopulateStaleCacheEntry() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
@ -397,12 +329,6 @@ public class TestProtocolRecommendations {
Mockito.verify(mockExecChain, Mockito.atMost(1)).proceed(Mockito.any(), Mockito.any());
}
/*
* "A transparent proxy SHOULD NOT modify an end-to-end header unless
* the definition of that header requires or specifically allows that."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
*/
private void testDoesNotModifyHeaderOnResponses(final String headerName) throws Exception {
final String headerValue = HttpTestUtils
.getCanonicalHeaderValue(originResponse, headerName);
@ -635,14 +561,6 @@ public class TestProtocolRecommendations {
testDoesNotModifyHeaderOnResponses("X-Extension");
}
/*
* "[HTTP/1.1 clients], If only a Last-Modified value has been provided
* by the origin server, SHOULD use that value in non-subrange cache-
* conditional requests (using If-Modified-Since)."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
*/
@Test
public void testUsesLastModifiedDateForCacheConditionalRequests() throws Exception {
final Instant twentySecondsAgo = now.plusSeconds(20);
@ -673,14 +591,6 @@ public class TestProtocolRecommendations {
MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-Modified-Since", lmDate));
}
/*
* "[HTTP/1.1 clients], if both an entity tag and a Last-Modified value
* have been provided by the origin server, SHOULD use both validators
* in cache-conditional requests. This allows both HTTP/1.0 and
* HTTP/1.1 caches to respond appropriately."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
*/
@Test
public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() throws Exception {
final Instant twentySecondsAgo = now.plusSeconds(20);
@ -714,14 +624,6 @@ public class TestProtocolRecommendations {
MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-None-Match", etag));
}
/*
* "If an origin server wishes to force a semantically transparent cache
* to validate every request, it MAY assign an explicit expiration time
* in the past. This means that the response is always stale, and so the
* cache SHOULD validate it before using it for subsequent requests."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.1
*/
@Test
public void testRevalidatesCachedResponseWithExpirationInThePast() throws Exception {
final Instant oneSecondAgo = now.minusSeconds(1);
@ -753,19 +655,6 @@ public class TestProtocolRecommendations {
assertEquals(HttpStatus.SC_OK, result.getCode());
}
/* "When a client tries to revalidate a cache entry, and the response
* it receives contains a Date header that appears to be older than the
* one for the existing entry, then the client SHOULD repeat the
* request unconditionally, and include
* Cache-Control: max-age=0
* to force any intermediate caches to validate their copies directly
* with the origin server, or
* Cache-Control: no-cache
* to force any intermediate caches to obtain a new copy from the
* origin server."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.6
*/
@Test
public void testRetriesValidationThatResultsInAnOlderDated304Response() throws Exception {
final Instant elevenSecondsAgo = now.minusSeconds(11);
@ -818,16 +707,6 @@ public class TestProtocolRecommendations {
assertFalse(captured.containsHeader("If-Unmodified-Since"));
}
/* "If an entity tag was assigned to a cached representation, the
* forwarded request SHOULD be conditional and include the entity
* tags in an If-None-Match header field from all its cache entries
* for the resource."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
* NOTE: This test no longer includes ETag headers "etag1" and "etag2"
* as they were causing issues with stack traces when printed to console
* or logged in the log file.
*/
@Test
public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
@ -879,13 +758,6 @@ public class TestProtocolRecommendations {
assertTrue(foundEtag1 && foundEtag2);
}
/* "If the entity-tag of the new response matches that of an existing
* entry, the new response SHOULD be used to processChallenge the header fields
* of the existing entry, and the result MUST be returned to the
* client."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void testResponseToExistingVariantsUpdatesEntry() throws Exception {
@ -948,6 +820,8 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("User-Agent", "agent2");
@ -957,153 +831,16 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
req3.setHeader("User-Agent", "agent2");
execute(req1);
execute(req2);
execute(req3);
}
/* "If any of the existing cache entries contains only partial content
* for the associated entity, its entity-tag SHOULD NOT be included in
* the If-None-Match header field unless the request is for a range
* that would be fully satisfied by that entry."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void variantNegotiationsDoNotIncludeEtagsForPartialResponses() throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
req1.setHeader("User-Agent", "agent1");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("ETag", "\"etag1\"");
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("User-Agent", "agent2");
req2.setHeader("Range", "bytes=0-49");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(HttpTestUtils.makeBody(50));
resp2.setHeader("Content-Length","50");
resp2.setHeader("Content-Range","bytes 0-49/100");
resp2.setHeader("Vary","User-Agent");
resp2.setHeader("ETag", "\"etag2\"");
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
req3.setHeader("User-Agent", "agent3");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("ETag", "\"etag3\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
execute(req3);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest captured = reqCapture.getValue();
final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.IF_NONE_MATCH);
while (it.hasNext()) {
final HeaderElement elt = it.next();
assertNotEquals("\"etag2\"", elt.toString());
}
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
/* "If a cache receives a successful response whose Content-Location
* field matches that of an existing cache entry for the same Request-
* URI, whose entity-tag differs from that of the existing entry, and
* whose Date is more recent than that of the existing entry, the
* existing entry SHOULD NOT be returned in response to future requests
* and SHOULD be deleted from the cache.
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void cachedEntryShouldNotBeUsedIfMoreRecentMentionInContentLocation() throws Exception {
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag", "\"old-etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpRequest req2 = new HttpPost("http://foo.example.com/bar");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"new-etag\"");
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader("Content-Location", "http://foo.example.com/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
execute(req1);
execute(req2);
execute(req3);
}
/*
* "This specifically means that responses from HTTP/1.0 servers for such
* URIs [those containing a '?' in the rel_path part] SHOULD NOT be taken
* from a cache."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
*/
@Test
public void responseToGetWithQueryFrom1_0OriginAndNoExpiresIsNotCached() throws Exception {
final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
resp2.setVersion(HttpVersion.HTTP_1_0);
resp2.setEntity(HttpTestUtils.makeBody(200));
resp2.setHeader("Content-Length","200");
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
@Test
public void responseToGetWithQueryFrom1_0OriginVia1_1ProxyAndNoExpiresIsNotCached() throws Exception {
final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
resp2.setVersion(HttpVersion.HTTP_1_0);
resp2.setEntity(HttpTestUtils.makeBody(200));
resp2.setHeader("Content-Length","200");
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader(HttpHeaders.VIA,"1.0 someproxy");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
/*
* "A cache that passes through requests for methods it does not
* understand SHOULD invalidate any entities referred to by the
* Request-URI."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
*/
@Test
public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@ -1112,6 +849,8 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("FROB", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
@ -1124,7 +863,6 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
execute(req1);
execute(req2);
final ClassicHttpResponse result = execute(req3);
@ -1184,13 +922,6 @@ public class TestProtocolRecommendations {
assertTrue(HttpTestUtils.semanticallyTransparent(resp5, result5));
}
/*
* "If a new cacheable response is received from a resource while any
* existing responses for the same resource are cached, the cache
* SHOULD use the new response to reply to the current request."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12
*/
@Test
public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
@ -1201,6 +932,8 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-age=0");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@ -1212,24 +945,12 @@ public class TestProtocolRecommendations {
final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
execute(req1);
execute(req2);
final ClassicHttpResponse result = execute(req3);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
/*
* "Many HTTP/1.0 cache implementations will treat an Expires value
* that is less than or equal to the response Date value as being
* equivalent to the Cache-Control response directive 'no-cache'.
* If an HTTP/1.1 cache receives such a response, and the response
* does not include a Cache-Control header field, it SHOULD consider
* the response to be non-cacheable in order to retain compatibility
* with HTTP/1.0 servers."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
*/
@Test
public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
@ -1240,6 +961,8 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@ -1247,7 +970,6 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req1);
final ClassicHttpResponse result = execute(req2);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
@ -1263,6 +985,8 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@ -1270,21 +994,11 @@ public class TestProtocolRecommendations {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req1);
final ClassicHttpResponse result = execute(req2);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
/*
* "To do this, the client may include the only-if-cached directive in
* a request. If it receives this directive, a cache SHOULD either
* respond using a cached entry that is consistent with the other
* constraints of the request, or respond with a 504 (Gateway Timeout)
* status."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
*/
@Test
public void cacheMissResultsIn504WithOnlyIfCached() throws Exception {
final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();

View File

@ -909,17 +909,6 @@ public class TestResponseCachingPolicy {
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
@Test
public void test303WithExplicitCachingHeadersUnderDefaultBehavior() {
// RFC 2616 says: 303 should not be cached
response.setCode(HttpStatus.SC_SEE_OTHER);
response.setHeader("Date", DateUtils.formatStandardDate(now));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(300)
.build();
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
}
@Test
public void test303WithExplicitCachingHeadersWhenPermittedByConfig() {
// HTTPbis working group says ok if explicitly indicated by
@ -1039,7 +1028,7 @@ public class TestResponseCachingPolicy {
request = new BasicHttpRequest("GET","/foo?s=bar");
// HTTPbis working group says ok if explicitly indicated by
// response headers
policy = new ResponseCachingPolicy(0, true, false, false, true);
policy = new ResponseCachingPolicy(0, true, false, true, true);
response.setCode(HttpStatus.SC_OK);
response.setHeader("Date", DateUtils.formatStandardDate(now));
assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
@ -1052,7 +1041,7 @@ public class TestResponseCachingPolicy {
response.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(0, true, false, false, false);
policy = new ResponseCachingPolicy(0, true, false, false, true);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(true)