Better ETag handling

This commit is contained in:
Oleg Kalnichevski 2024-01-09 14:30:04 +01:00
parent 07586902ec
commit a1e8e9082e
12 changed files with 132 additions and 90 deletions

View File

@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
@ -79,6 +80,8 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
private final AtomicReference<Instant> expiresRef;
private final AtomicReference<Instant> lastModifiedRef;
private final AtomicReference<ETag> eTagRef;
/**
* Internal constructor that makes no validation of the input parameters and makes
* no copies of the original client request and the origin response.
@ -107,6 +110,7 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
this.dateRef = new AtomicReference<>();
this.expiresRef = new AtomicReference<>();
this.lastModifiedRef = new AtomicReference<>();
this.eTagRef = new AtomicReference<>();
}
/**
@ -179,6 +183,7 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
this.dateRef = new AtomicReference<>();
this.expiresRef = new AtomicReference<>();
this.lastModifiedRef = new AtomicReference<>();
this.eTagRef = new AtomicReference<>();
}
/**
@ -396,6 +401,23 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
return getInstant(lastModifiedRef, HttpHeaders.LAST_MODIFIED);
}
/**
* @since 5.4
*/
public ETag getETag() {
ETag eTag = eTagRef.get();
if (eTag == null) {
eTag = ETag.get(this);
if (eTag == null) {
return null;
}
if (!eTagRef.compareAndSet(null, eTag)) {
eTag = eTagRef.get();
}
}
return eTag;
}
/**
* Returns the {@link Resource} containing the origin response body.
*/

View File

@ -30,7 +30,6 @@ import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -55,6 +54,7 @@ import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.impl.ExecSupport;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.schedule.SchedulingStrategy;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.CancellableDependency;
@ -1206,16 +1206,17 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final Collection<CacheHit> variants) {
final String exchangeId = scope.exchangeId;
final CancellableDependency operation = scope.cancellableDependency;
final Map<String, CacheHit> variantMap = new HashMap<>();
final Map<ETag, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) {
final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
if (header != null) {
variantMap.put(header.getValue(), variant);
final ETag eTag = variant.entry.getETag();
if (eTag != null) {
variantMap.put(eTag, variant);
}
}
final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
BasicRequestBuilder.copy(request).build(),
new ArrayList<>(variantMap.keySet()));
request,
variantMap.keySet());
final Instant requestDate = getCurrentDate();
chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
@ -1270,14 +1271,13 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
} else {
final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG);
if (resultEtagHeader == null) {
final ETag resultEtag = ETag.get(backendResponse);
if (resultEtag == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} 304 response did not contain ETag", exchangeId);
}
callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
} else {
final String resultEtag = resultEtagHeader.getValue();
final CacheHit match = variantMap.get(resultEtag);
if (match == null) {
if (LOG.isDebugEnabled()) {

View File

@ -33,7 +33,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@ -46,11 +45,11 @@ import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.impl.Operations;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.concurrent.CallbackContribution;
import org.apache.hc.core5.concurrent.Cancellable;
import org.apache.hc.core5.concurrent.ComplexCancellable;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
@ -520,10 +519,10 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Evicting root cache entry {}", rootKey);
}
final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG);
final Header newETag = response.getFirstHeader(HttpHeaders.ETAG);
final ETag existingETag = root.getETag();
final ETag newETag = ETag.get(response);
if (existingETag != null && newETag != null &&
!Objects.equals(existingETag.getValue(), newETag.getValue()) &&
!ETag.strongCompare(existingETag, newETag) &&
!HttpCacheEntry.isNewer(root, response)) {
evictAll(root, rootKey);
}

View File

@ -32,7 +32,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
@ -43,7 +42,7 @@ import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.http.Header;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
@ -329,10 +328,10 @@ class BasicHttpCache implements HttpCache {
if (root == null) {
return;
}
final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG);
final Header newETag = response.getFirstHeader(HttpHeaders.ETAG);
final ETag existingETag = root.getETag();
final ETag newETag = ETag.get(response);
if (existingETag != null && newETag != null &&
!Objects.equals(existingETag.getValue(), newETag.getValue()) &&
!ETag.strongCompare(existingETag, newETag) &&
!HttpCacheEntry.isNewer(root, response)) {
evictAll(root, rootKey);
}

View File

@ -33,6 +33,7 @@ import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
@ -108,9 +109,9 @@ class CachedHttpResponseGenerator {
// - ETag and/or Content-Location, if the header would have been sent
// in a 200 response to the same request
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG);
if (etagHeader != null) {
response.addHeader(etagHeader);
final ETag eTag = entry.getETag();
if (eTag != null) {
response.addHeader(new BasicHeader(HttpHeaders.ETAG, eTag.toString()));
}
final Header contentLocationHeader = entry.getFirstHeader(HttpHeaders.CONTENT_LOCATION);
@ -142,7 +143,7 @@ class CachedHttpResponseGenerator {
//above listed fields unless said metadata exists for the purpose of
//guiding cache updates (e.g., Last-Modified might be useful if the
//response does not have an ETag field).
if (etagHeader == null) {
if (eTag == null) {
final Header lastModifiedHeader = entry.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (lastModifiedHeader != null) {
response.addHeader(lastModifiedHeader);

View File

@ -41,8 +41,8 @@ import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.RequestCacheControl;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
@ -332,13 +332,14 @@ class CachedResponseSuitabilityChecker {
* @return boolean does the etag validator match
*/
boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG);
final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HttpHeaders.IF_NONE_MATCH);
final ETag etag = entry.getETag();
if (etag == null) {
return false;
}
final Iterator<String> it = MessageSupport.iterateTokens(request, HttpHeaders.IF_NONE_MATCH);
while (it.hasNext()) {
final HeaderElement elt = it.next();
final String reqEtag = elt.toString();
if (("*".equals(reqEtag) && etag != null) || reqEtag.equals(etag)) {
final String token = it.next();
if ("*".equals(token) || ETag.weakCompare(etag, ETag.parse(token))) {
return true;
}
}

View File

@ -29,7 +29,6 @@ package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -49,6 +48,7 @@ import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.impl.ExecSupport;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
@ -605,17 +605,17 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
final List<CacheHit> variants) throws IOException, HttpException {
final String exchangeId = scope.exchangeId;
final Map<String, CacheHit> variantMap = new HashMap<>();
final Map<ETag, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) {
final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
if (header != null) {
variantMap.put(header.getValue(), variant);
final ETag eTag = variant.entry.getETag();
if (eTag != null) {
variantMap.put(eTag, variant);
}
}
final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
request,
new ArrayList<>(variantMap.keySet()));
variantMap.keySet());
final Instant requestDate = getCurrentDate();
final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
@ -629,15 +629,14 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
backendResponse.close();
}
final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG);
if (resultEtagHeader == null) {
final ETag resultEtag = ETag.get(backendResponse);
if (resultEtag == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} 304 response did not contain ETag", exchangeId);
}
return callBackend(target, request, scope, chain);
}
final String resultEtag = resultEtagHeader.getValue();
final CacheHit match = variantMap.get(resultEtag);
if (match == null) {
if (LOG.isDebugEnabled()) {

View File

@ -26,16 +26,18 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.util.List;
import java.util.Collection;
import org.apache.hc.client5.http.cache.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.function.Factory;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.http.message.BufferedHeader;
import org.apache.hc.core5.util.CharArrayBuffer;
class ConditionalRequestBuilder<T extends HttpRequest> {
@ -59,9 +61,9 @@ class ConditionalRequestBuilder<T extends HttpRequest> {
public T buildConditionalRequest(final ResponseCacheControl cacheControl, final T request, final HttpCacheEntry cacheEntry) {
final T newRequest = messageCopier.create(request);
final Header eTag = cacheEntry.getFirstHeader(HttpHeaders.ETAG);
final ETag eTag = cacheEntry.getETag();
if (eTag != null) {
newRequest.setHeader(HttpHeaders.IF_NONE_MATCH, eTag.getValue());
newRequest.setHeader(HttpHeaders.IF_NONE_MATCH, eTag.toString());
}
final Header lastModified = cacheEntry.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (lastModified != null) {
@ -85,9 +87,20 @@ class ConditionalRequestBuilder<T extends HttpRequest> {
* @param variants
* @return the wrapped request
*/
public T buildConditionalRequestFromVariants(final T request, final List<String> variants) {
public T buildConditionalRequestFromVariants(final T request, final Collection<ETag> variants) {
final T newRequest = messageCopier.create(request);
newRequest.setHeader(MessageSupport.headerOfTokens(HttpHeaders.IF_NONE_MATCH, variants));
final CharArrayBuffer buffer = new CharArrayBuffer(256);
buffer.append(HttpHeaders.IF_NONE_MATCH);
buffer.append(": ");
int i = 0;
for (final ETag variant : variants) {
if (i > 0) {
buffer.append(", ");
}
variant.format(buffer);
i++;
}
newRequest.setHeader(BufferedHeader.create(buffer));
return newRequest;
}

View File

@ -50,6 +50,7 @@ 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.HttpOptions;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
@ -997,20 +998,20 @@ public class TestCachingExecChain {
final Instant now = Instant.now();
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
req1.addHeader("If-None-Match", "etag");
req1.addHeader("If-None-Match", "\"etag\"");
final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
req2.addHeader("If-None-Match", "etag");
req2.addHeader("If-None-Match", "\"etag\"");
final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag");
resp1.setHeader("Etag", "\"etag\"");
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag");
resp1.setHeader("Etag", "\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
@ -1020,9 +1021,9 @@ public class TestCachingExecChain {
final ClassicHttpResponse result2 = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
Assertions.assertEquals("etag", result1.getFirstHeader("Etag").getValue());
Assertions.assertEquals(new ETag("etag"), ETag.get(result1));
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue());
Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
}
@Test
@ -1031,21 +1032,21 @@ public class TestCachingExecChain {
final Instant now = Instant.now();
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
req1.addHeader("If-None-Match", "etag");
req1.addHeader("If-None-Match", "\"etag\"");
final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
req2.addHeader("If-None-Match", "etag");
req2.addHeader("If-None-Match", "\"etag\"");
final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag");
resp1.setHeader("Etag", "\"etag\"");
resp1.setHeader("Vary", "Accept-Encoding");
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag");
resp1.setHeader("Etag", "\"etag\"");
resp1.setHeader("Vary", "Accept-Encoding");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
@ -1057,9 +1058,9 @@ public class TestCachingExecChain {
final ClassicHttpResponse result2 = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
Assertions.assertEquals("etag", result1.getFirstHeader("Etag").getValue());
Assertions.assertEquals(new ETag("etag"), ETag.get(result1));
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue());
Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
}
@Test

View File

@ -27,19 +27,26 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.hc.client5.http.HeadersMatcher;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.RequestCacheControl;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -76,11 +83,11 @@ public class TestConditionalRequestBuilder {
Assertions.assertEquals(theUri, newRequest.getRequestUri());
Assertions.assertEquals(2, newRequest.getHeaders().length);
Assertions.assertEquals("Accept-Encoding", newRequest.getHeaders()[0].getName());
Assertions.assertEquals("gzip", newRequest.getHeaders()[0].getValue());
Assertions.assertEquals("If-Modified-Since", newRequest.getHeaders()[1].getName());
Assertions.assertEquals(lastModified, newRequest.getHeaders()[1].getValue());
MatcherAssert.assertThat(
newRequest.getHeaders(),
HeadersMatcher.same(
new BasicHeader("Accept-Encoding", "gzip"),
new BasicHeader("If-Modified-Since", lastModified)));
}
@Test
@ -110,14 +117,16 @@ public class TestConditionalRequestBuilder {
public void testBuildConditionalRequestWithETag() {
final String theMethod = "GET";
final String theUri = "/theuri";
final String theETag = "this is my eTag";
final String theETag = "\"this is my eTag\"";
final HttpRequest basicRequest = new BasicHttpRequest(theMethod, theUri);
basicRequest.addHeader("Accept-Encoding", "gzip");
final Instant now = Instant.now();
final Header[] headers = new Header[] {
new BasicHeader("Date", DateUtils.formatStandardDate(Instant.now())),
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now())),
new BasicHeader("Date", DateUtils.formatStandardDate(now)),
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(now)),
new BasicHeader("ETag", theETag) };
final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(headers);
@ -128,13 +137,12 @@ public class TestConditionalRequestBuilder {
Assertions.assertEquals(theMethod, newRequest.getMethod());
Assertions.assertEquals(theUri, newRequest.getRequestUri());
Assertions.assertEquals(3, newRequest.getHeaders().length);
Assertions.assertEquals("Accept-Encoding", newRequest.getHeaders()[0].getName());
Assertions.assertEquals("gzip", newRequest.getHeaders()[0].getValue());
Assertions.assertEquals("If-None-Match", newRequest.getHeaders()[1].getName());
Assertions.assertEquals(theETag, newRequest.getHeaders()[1].getValue());
MatcherAssert.assertThat(
newRequest.getHeaders(),
HeadersMatcher.same(
new BasicHeader("Accept-Encoding", "gzip"),
new BasicHeader("If-None-Match", theETag),
new BasicHeader("If-Modified-Since", DateUtils.formatStandardDate(now))));
}
@Test
@ -260,25 +268,21 @@ public class TestConditionalRequestBuilder {
@Test
public void testBuildConditionalRequestFromVariants() throws Exception {
final String etag1 = "\"123\"";
final String etag2 = "\"456\"";
final String etag3 = "\"789\"";
final ETag etag1 = new ETag("123");
final ETag etag2 = new ETag("456");
final ETag etag3 = new ETag("789");
final List<String> variantEntries = Arrays.asList(etag1, etag2, etag3);
final List<ETag> variantEntries = Arrays.asList(etag1, etag2, etag3);
final HttpRequest conditional = impl.buildConditionalRequestFromVariants(request, variantEntries);
// seems like a lot of work, but necessary, check for existence and exclusiveness
String ifNoneMatch = conditional.getFirstHeader(HttpHeaders.IF_NONE_MATCH).getValue();
Assertions.assertTrue(ifNoneMatch.contains(etag1));
Assertions.assertTrue(ifNoneMatch.contains(etag2));
Assertions.assertTrue(ifNoneMatch.contains(etag3));
ifNoneMatch = ifNoneMatch.replace(etag1, "");
ifNoneMatch = ifNoneMatch.replace(etag2, "");
ifNoneMatch = ifNoneMatch.replace(etag3, "");
ifNoneMatch = ifNoneMatch.replace(",","");
ifNoneMatch = ifNoneMatch.replace(" ", "");
Assertions.assertEquals(ifNoneMatch, "");
final Iterator<String> it = MessageSupport.iterateTokens(conditional, HttpHeaders.IF_NONE_MATCH);
final List<ETag> etags = new ArrayList<>();
while (it.hasNext()) {
etags.add(ETag.parse(it.next()));
}
MatcherAssert.assertThat(etags, Matchers.containsInAnyOrder(etag1, etag2, etag3));
}
}

View File

@ -33,6 +33,8 @@ import java.io.IOException;
import java.time.Instant;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.client5.http.validator.ETag;
import org.apache.hc.client5.http.validator.ValidatorType;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.EntityDetails;
@ -106,10 +108,10 @@ public class RequestIfRange implements HttpRequestInterceptor {
throw new ProtocolException("Request with 'If-Range' header must also contain a 'Range' header.");
}
final Header eTag = request.getFirstHeader(HttpHeaders.ETAG);
final ETag eTag = ETag.get(request);
// If there's a weak ETag in the If-Range header, throw an exception
if (eTag != null && eTag.getValue().startsWith("W/")) {
if (eTag != null && eTag.getType() == ValidatorType.WEAK) {
throw new ProtocolException("'If-Range' header must not contain a weak entity tag.");
}

View File

@ -31,6 +31,7 @@ import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.MessageHeaders;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.CharArrayBuffer;