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:
parent
c7d79190e6
commit
1b0e9cf7b0
|
@ -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 "backend"
|
||||
* 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)
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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:");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue