Description
Affects: 5.3.21
Stack overflow question
Expected behaviour:
If-None-Match
has precedence when If-None-Match
is used in combination with If-Modified-Since
.
The function checkNotModified
in org.springframework.web.context.request.ServletWebRequest
references the expected order of precedence with the following comment:
// Evaluate conditions in order of precedence.
// See https://tools.ietf.org/html/rfc7232#section-6
checkNotModified methods
@Override
public boolean checkNotModified(long lastModifiedTimestamp) {
return checkNotModified(null, lastModifiedTimestamp);
}
@Override
public boolean checkNotModified(String etag) {
return checkNotModified(etag, -1);
}
@Override
public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {
HttpServletResponse response = getResponse();
if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
return this.notModified;
}
// Evaluate conditions in order of precedence.
// See https://tools.ietf.org/html/rfc7232#section-6
if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
if (this.notModified && response != null) {
response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
}
return this.notModified;
}
boolean validated = validateIfNoneMatch(etag);
if (!validated) {
validateIfModifiedSince(lastModifiedTimestamp);
}
// Update response
if (response != null) {
boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
if (this.notModified) {
response.setStatus(isHttpGetOrHead ?
HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
}
if (isHttpGetOrHead) {
if (lastModifiedTimestamp > 0 && parseDateValue(response.getHeader(HttpHeaders.LAST_MODIFIED)) == -1) {
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp);
}
if (StringUtils.hasLength(etag) && response.getHeader(HttpHeaders.ETAG) == null) {
response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag));
}
}
}
return this.notModified;
}
Observed behaviour
The server returns 304 Not Modified
for an invalid etag when both the If-None-Match
and If-Modified-Since
-header is set.
The response is updated with status NOT MODIFIED
when checkNotModified
is called from handleRequest
in org.springframework.web.servlet.resource.ResourceHttpRequestHandler
and lastModifiedTimestamp
indicates that the resources is not modified.
The response is not updated again when checkNotModified
is called from updateResponse
in org.springframework.web.filter.ShallowEtagHeaderFilter
with an etag value indicating that the resource is modified.
Edit: I'm honestly quite confused. I tried to write a test to see if the response code is updated correctly in ShallowEtagHeaderFilter. Looking on the test, it seems that it handles the situation correctly. When debugging locally, it looks like the response status code is still
304 Not Modified
. Furthermore, it should not matter if it handles the status code correctly whenShallowEtagHeaderFilter
is executed beforeResourceHttpRequestHandler
which updates the response status code to304 Not Modified
anyway.
Test coverage
checkNotModified
is covered by the test IfNoneMatchAndIfNotModifiedSinceShouldNotMatchWhenDifferentETag
. However, this test covers the case where checkNotModified
is called with both eTag
and lastModifiedTimestamp
. In the case described, checkNotModified
is called sequentially by overloaded methods. The overloaded method call with lastModifiedTimestamp
alters the response status to 304 Not Modified
, and it seems that it makes isEligibleForEtag in ShallowEtagHeaderFilter return false if the status code is 304
, and the call to checkNotModified
from updateResponse in shallowEtagHeaderFilter does not update the response from 304
to 200 OK
if the etag has changed.
I currently see no test in ShallowEtagHeaderFilterTests asserting that the response code is 200 OK
if a response has an invalid etag and response code 304 Not Modified
.
Problem
After rollback to an earlier deployment, clients will send a lastModifiedTimestamp
newer than the content on the server and an invalid etag. Since If-None-Match
doesn't have precedence as expected, the client recieves a 304 NOT MODIFIED
when a 200 OK
was expected.
Work-around
Problem can be mitigated by not using If-Modified-Since
. E.g. utilizing setUseLastModified(false)
on a resource handler.
Reproduction
Send a GET
-request with an invalid etag and lastModifiedTimestamp
in the future.