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 java.util.stream.Collectors;
import org.apache.hc.client5.http.utils.DateUtils; 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.Contract;
import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior; 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> expiresRef;
private final AtomicReference<Instant> lastModifiedRef; private final AtomicReference<Instant> lastModifiedRef;
private final AtomicReference<ETag> eTagRef;
/** /**
* Internal constructor that makes no validation of the input parameters and makes * Internal constructor that makes no validation of the input parameters and makes
* no copies of the original client request and the origin response. * 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.dateRef = new AtomicReference<>();
this.expiresRef = new AtomicReference<>(); this.expiresRef = new AtomicReference<>();
this.lastModifiedRef = 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.dateRef = new AtomicReference<>();
this.expiresRef = new AtomicReference<>(); this.expiresRef = new AtomicReference<>();
this.lastModifiedRef = 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); 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. * 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.io.InterruptedIOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; 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.impl.ExecSupport;
import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.schedule.SchedulingStrategy; 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.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.CancellableDependency; import org.apache.hc.core5.concurrent.CancellableDependency;
@ -1206,16 +1206,17 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final Collection<CacheHit> variants) { final Collection<CacheHit> variants) {
final String exchangeId = scope.exchangeId; final String exchangeId = scope.exchangeId;
final CancellableDependency operation = scope.cancellableDependency; final CancellableDependency operation = scope.cancellableDependency;
final Map<String, CacheHit> variantMap = new HashMap<>(); final Map<ETag, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) { for (final CacheHit variant : variants) {
final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG); final ETag eTag = variant.entry.getETag();
if (header != null) { if (eTag != null) {
variantMap.put(header.getValue(), variant); variantMap.put(eTag, variant);
} }
} }
final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants( final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
BasicRequestBuilder.copy(request).build(), request,
new ArrayList<>(variantMap.keySet())); variantMap.keySet());
final Instant requestDate = getCurrentDate(); final Instant requestDate = getCurrentDate();
chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() { chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
@ -1270,14 +1271,13 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) { if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback); callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
} else { } else {
final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG); final ETag resultEtag = ETag.get(backendResponse);
if (resultEtagHeader == null) { if (resultEtag == null) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("{} 304 response did not contain ETag", exchangeId); LOG.debug("{} 304 response did not contain ETag", exchangeId);
} }
callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed); callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
} else { } else {
final String resultEtag = resultEtagHeader.getValue();
final CacheHit match = variantMap.get(resultEtag); final CacheHit match = variantMap.get(resultEtag);
if (match == null) { if (match == null) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {

View File

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

View File

@ -32,7 +32,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import org.apache.hc.client5.http.cache.HttpCacheCASOperation; 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.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory; import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException; 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.HttpHeaders;
import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequest;
@ -329,10 +328,10 @@ class BasicHttpCache implements HttpCache {
if (root == null) { if (root == null) {
return; return;
} }
final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG); final ETag existingETag = root.getETag();
final Header newETag = response.getFirstHeader(HttpHeaders.ETAG); final ETag newETag = ETag.get(response);
if (existingETag != null && newETag != null && if (existingETag != null && newETag != null &&
!Objects.equals(existingETag.getValue(), newETag.getValue()) && !ETag.strongCompare(existingETag, newETag) &&
!HttpCacheEntry.isNewer(root, response)) { !HttpCacheEntry.isNewer(root, response)) {
evictAll(root, rootKey); 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.Resource;
import org.apache.hc.client5.http.cache.ResourceIOException; import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.utils.DateUtils; 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.ContentType;
import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders; 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 // - ETag and/or Content-Location, if the header would have been sent
// in a 200 response to the same request // in a 200 response to the same request
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG); final ETag eTag = entry.getETag();
if (etagHeader != null) { if (eTag != null) {
response.addHeader(etagHeader); response.addHeader(new BasicHeader(HttpHeaders.ETAG, eTag.toString()));
} }
final Header contentLocationHeader = entry.getFirstHeader(HttpHeaders.CONTENT_LOCATION); 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 //above listed fields unless said metadata exists for the purpose of
//guiding cache updates (e.g., Last-Modified might be useful if the //guiding cache updates (e.g., Last-Modified might be useful if the
//response does not have an ETag field). //response does not have an ETag field).
if (etagHeader == null) { if (eTag == null) {
final Header lastModifiedHeader = entry.getFirstHeader(HttpHeaders.LAST_MODIFIED); final Header lastModifiedHeader = entry.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (lastModifiedHeader != null) { if (lastModifiedHeader != null) {
response.addHeader(lastModifiedHeader); 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.RequestCacheControl;
import org.apache.hc.client5.http.cache.ResponseCacheControl; import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.client5.http.utils.DateUtils; 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.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpStatus;
@ -332,13 +332,14 @@ class CachedResponseSuitabilityChecker {
* @return boolean does the etag validator match * @return boolean does the etag validator match
*/ */
boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) { boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG); final ETag etag = entry.getETag();
final String etag = (etagHeader != null) ? etagHeader.getValue() : null; if (etag == null) {
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HttpHeaders.IF_NONE_MATCH); return false;
}
final Iterator<String> it = MessageSupport.iterateTokens(request, HttpHeaders.IF_NONE_MATCH);
while (it.hasNext()) { while (it.hasNext()) {
final HeaderElement elt = it.next(); final String token = it.next();
final String reqEtag = elt.toString(); if ("*".equals(token) || ETag.weakCompare(etag, ETag.parse(token))) {
if (("*".equals(reqEtag) && etag != null) || reqEtag.equals(etag)) {
return true; return true;
} }
} }

View File

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

View File

@ -26,16 +26,18 @@
*/ */
package org.apache.hc.client5.http.impl.cache; 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.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpCacheEntry; import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.ResponseCacheControl; 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.function.Factory;
import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest; 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> { 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) { public T buildConditionalRequest(final ResponseCacheControl cacheControl, final T request, final HttpCacheEntry cacheEntry) {
final T newRequest = messageCopier.create(request); final T newRequest = messageCopier.create(request);
final Header eTag = cacheEntry.getFirstHeader(HttpHeaders.ETAG); final ETag eTag = cacheEntry.getETag();
if (eTag != null) { 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); final Header lastModified = cacheEntry.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (lastModified != null) { if (lastModified != null) {
@ -85,9 +87,20 @@ class ConditionalRequestBuilder<T extends HttpRequest> {
* @param variants * @param variants
* @return the wrapped request * @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); 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; 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.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpOptions; import org.apache.hc.client5.http.classic.methods.HttpOptions;
import org.apache.hc.client5.http.utils.DateUtils; 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.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.Header;
@ -997,20 +998,20 @@ public class TestCachingExecChain {
final Instant now = Instant.now(); final Instant now = Instant.now();
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/"); 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/"); 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(); final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now)); resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag"); resp1.setHeader("Etag", "\"etag\"");
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response(); final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
resp2.setHeader("Date", DateUtils.formatStandardDate(now)); resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader("Cache-Control", "max-age=1"); 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); Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
@ -1020,9 +1021,9 @@ public class TestCachingExecChain {
final ClassicHttpResponse result2 = execute(req2); final ClassicHttpResponse result2 = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode()); 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(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue()); Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
} }
@Test @Test
@ -1031,21 +1032,21 @@ public class TestCachingExecChain {
final Instant now = Instant.now(); final Instant now = Instant.now();
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/"); 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/"); 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(); final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now)); resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag"); resp1.setHeader("Etag", "\"etag\"");
resp1.setHeader("Vary", "Accept-Encoding"); resp1.setHeader("Vary", "Accept-Encoding");
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response(); final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
resp2.setHeader("Date", DateUtils.formatStandardDate(now)); resp2.setHeader("Date", DateUtils.formatStandardDate(now));
resp2.setHeader("Cache-Control", "max-age=1"); resp2.setHeader("Cache-Control", "max-age=1");
resp1.setHeader("Etag", "etag"); resp1.setHeader("Etag", "\"etag\"");
resp1.setHeader("Vary", "Accept-Encoding"); resp1.setHeader("Vary", "Accept-Encoding");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1); Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
@ -1057,9 +1058,9 @@ public class TestCachingExecChain {
final ClassicHttpResponse result2 = execute(req2); final ClassicHttpResponse result2 = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode()); 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(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
Assertions.assertEquals("etag", result2.getFirstHeader("Etag").getValue()); Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
} }
@Test @Test

View File

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

View File

@ -33,6 +33,8 @@ import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import org.apache.hc.client5.http.utils.DateUtils; 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.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.EntityDetails; 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."); 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 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."); 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.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.Header; 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.http.MessageHeaders;
import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.CharArrayBuffer; import org.apache.hc.core5.util.CharArrayBuffer;