Fix for protocol recommendation:
"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 git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1058762 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
bdf4174033
commit
17aa988f41
|
@ -84,6 +84,8 @@ class ResponseProtocolCompliance {
|
||||||
|
|
||||||
ensure206ContainsDateHeader(response);
|
ensure206ContainsDateHeader(response);
|
||||||
|
|
||||||
|
ensure304DoesNotContainExtraEntityHeaders(response);
|
||||||
|
|
||||||
identityIsNotUsedInContentEncoding(response);
|
identityIsNotUsedInContentEncoding(response);
|
||||||
|
|
||||||
warningsWithNonMatchingWarnDatesAreRemoved(response);
|
warningsWithNonMatchingWarnDatesAreRemoved(response);
|
||||||
|
@ -212,6 +214,18 @@ class ResponseProtocolCompliance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensure304DoesNotContainExtraEntityHeaders(HttpResponse response) {
|
||||||
|
String[] disallowedEntityHeaders = { "Allow", "Content-Encoding",
|
||||||
|
"Content-Language", "Content-Length", "Content-MD5",
|
||||||
|
"Content-Range", "Content-Type", "Last-Modified"
|
||||||
|
};
|
||||||
|
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
|
||||||
|
for(String hdr : disallowedEntityHeaders) {
|
||||||
|
response.removeHeaders(hdr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean backendResponseMustNotHaveBody(HttpRequest request, HttpResponse backendResponse) {
|
private boolean backendResponseMustNotHaveBody(HttpRequest request, HttpResponse backendResponse) {
|
||||||
return HeaderConstants.HEAD_METHOD.equals(request.getRequestLine().getMethod())
|
return HeaderConstants.HEAD_METHOD.equals(request.getRequestLine().getMethod())
|
||||||
|| backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT
|
|| backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT
|
||||||
|
|
|
@ -77,6 +77,16 @@ public class HttpTestUtils {
|
||||||
"Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
|
"Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
|
||||||
"User-Agent", "Vary" };
|
"User-Agent", "Vary" };
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "Entity-header fields define metainformation about the entity-body or,
|
||||||
|
* if no body is present, about the resource identified by the request."
|
||||||
|
*
|
||||||
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
|
||||||
|
*/
|
||||||
|
public static final String[] ENTITY_HEADERS = { "Allow", "Content-Encoding",
|
||||||
|
"Content-Language", "Content-Length", "Content-Location", "Content-MD5",
|
||||||
|
"Content-Range", "Content-Type", "Expires", "Last-Modified" };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Determines whether the given header name is considered a hop-by-hop
|
* Determines whether the given header name is considered a hop-by-hop
|
||||||
* header.
|
* header.
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.apache.http.HttpStatus;
|
||||||
import org.apache.http.HttpVersion;
|
import org.apache.http.HttpVersion;
|
||||||
import static org.apache.http.impl.cookie.DateUtils.formatDate;
|
import static org.apache.http.impl.cookie.DateUtils.formatDate;
|
||||||
|
|
||||||
|
import org.apache.http.client.ClientProtocolException;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
|
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
|
||||||
import org.apache.http.message.BasicHttpRequest;
|
import org.apache.http.message.BasicHttpRequest;
|
||||||
|
@ -64,6 +65,7 @@ public class TestProtocolRecommendations extends AbstractProtocolTest {
|
||||||
private Date tenSecondsFromNow;
|
private Date tenSecondsFromNow;
|
||||||
private Date now;
|
private Date now;
|
||||||
private Date tenSecondsAgo;
|
private Date tenSecondsAgo;
|
||||||
|
private Date twoMinutesAgo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Before
|
@Before
|
||||||
|
@ -71,6 +73,7 @@ public class TestProtocolRecommendations extends AbstractProtocolTest {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
now = new Date();
|
now = new Date();
|
||||||
tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000L);
|
||||||
tenSecondsFromNow = new Date(now.getTime() + 10 * 1000L);
|
tenSecondsFromNow = new Date(now.getTime() + 10 * 1000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +103,294 @@ public class TestProtocolRecommendations extends AbstractProtocolTest {
|
||||||
assertFalse(foundIdentity);
|
assertFalse(foundIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "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(
|
||||||
|
String headerName, String headerValue, String validatorHeader,
|
||||||
|
String validator, String conditionalHeader) throws Exception,
|
||||||
|
IOException {
|
||||||
|
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
|
||||||
|
HttpResponse resp1 = HttpTestUtils.make200Response();
|
||||||
|
resp1.setHeader("Cache-Control","max-age=3600");
|
||||||
|
resp1.setHeader(validatorHeader, validator);
|
||||||
|
resp1.setHeader(headerName, headerValue);
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(resp1);
|
||||||
|
|
||||||
|
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
|
||||||
|
req2.setHeader(conditionalHeader, validator);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1);
|
||||||
|
HttpResponse result = impl.execute(host, req2);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
if (HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode()) {
|
||||||
|
assertNull(result.getFirstHeader(headerName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
String headerName, String headerValue) throws Exception,
|
||||||
|
IOException {
|
||||||
|
cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
|
||||||
|
headerValue, "ETag", "\"etag\"", "If-None-Match");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
String headerName, String headerValue) throws Exception,
|
||||||
|
IOException {
|
||||||
|
cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
|
||||||
|
headerValue, "Last-Modified", formatDate(twoMinutesAgo),
|
||||||
|
"If-Modified-Since");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainAllow()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Allow", "GET,HEAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainAllow()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Allow", "GET,HEAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentEncoding()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Encoding", "gzip");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentEncoding()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Encoding", "gzip");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentLanguage()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Language", "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLanguage()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Language", "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongValidatorShouldNotContainContentLength()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Length", "128");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLength()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Length", "128");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongValidatorShouldNotContainContentMD5()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
|
||||||
|
String validatorHeader, String validator, String conditionalHeader)
|
||||||
|
throws Exception, IOException, ClientProtocolException {
|
||||||
|
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
|
||||||
|
req1.setHeader("Range","bytes=0-127");
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, 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");
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(resp1);
|
||||||
|
|
||||||
|
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
|
||||||
|
req2.setHeader("If-Range", validator);
|
||||||
|
req2.setHeader("Range","bytes=0-127");
|
||||||
|
req2.setHeader(conditionalHeader, validator);
|
||||||
|
|
||||||
|
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
|
||||||
|
resp2.setHeader("Date", formatDate(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
|
||||||
|
Capture<HttpRequest> cap = new Capture<HttpRequest>();
|
||||||
|
expect(mockBackend.execute(same(host), capture(cap), (HttpContext)isNull()))
|
||||||
|
.andReturn(resp2).times(0,1);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1);
|
||||||
|
HttpResponse result = impl.execute(host, req2);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
if (!cap.hasCaptured()
|
||||||
|
&& HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode()) {
|
||||||
|
// cache generated a 304
|
||||||
|
assertNull(result.getFirstHeader("Content-Range"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
|
||||||
|
"ETag", "\"etag\"", "If-None-Match");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
|
||||||
|
"Last-Modified", formatDate(twoMinutesAgo), "If-Modified-Since");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Type", "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentType()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Content-Type", "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainLastModified()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
|
||||||
|
"Last-Modified", formatDate(tenSecondsAgo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified()
|
||||||
|
throws Exception {
|
||||||
|
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
|
||||||
|
"Last-Modified", formatDate(twoMinutesAgo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
String entityHeader, String entityHeaderValue) throws Exception,
|
||||||
|
IOException {
|
||||||
|
HttpRequest req = HttpTestUtils.makeDefaultRequest();
|
||||||
|
req.setHeader("If-None-Match", "\"etag\"");
|
||||||
|
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
|
||||||
|
resp.setHeader("Date", formatDate(now));
|
||||||
|
resp.setHeader("Etag", "\"etag\"");
|
||||||
|
resp.setHeader(entityHeader, entityHeaderValue);
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(resp);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, req);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
assertNull(result.getFirstHeader(entityHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripAllowFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Allow", "GET,HEAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentEncodingFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Content-Encoding", "gzip");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentLanguageFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Content-Language", "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentLengthFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Content-Length", "128");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentMD5FromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentTypeFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Content-Type", "text/html;charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripContentRangeFromOrigin304ResponseToStringValidation()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = HttpTestUtils.makeDefaultRequest();
|
||||||
|
req.setHeader("If-Range","\"etag\"");
|
||||||
|
req.setHeader("Range","bytes=0-127");
|
||||||
|
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
|
||||||
|
resp.setHeader("Date", formatDate(now));
|
||||||
|
resp.setHeader("ETag", "\"etag\"");
|
||||||
|
resp.setHeader("Content-Range", "bytes 0-127/256");
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(resp);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
HttpResponse result = impl.execute(host, req);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
assertNull(result.getFirstHeader("Content-Range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldStripLastModifiedFromOrigin304ResponseToStrongValidation()
|
||||||
|
throws Exception {
|
||||||
|
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
|
||||||
|
"Last-Modified", formatDate(twoMinutesAgo));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "For this reason, a cache SHOULD NOT return a stale response if the
|
* "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
|
* client explicitly requests a first-hand or fresh one, unless it is
|
||||||
|
@ -1461,4 +1752,5 @@ public class TestProtocolRecommendations extends AbstractProtocolTest {
|
||||||
|
|
||||||
assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
|
assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue