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
even 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
Observed behaviour
The response is updated with status NOT MODIFIED
when checkNotModified
is called from handleRequest
in org.springframework.web.servlet.resource.ResourceHttpRequestHandler
whenlastModifiedTimestamp
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.
As a result, the server returns 304 NOT MODIFIED
for an invalid etag when both the If-None-Match
and If-Modified-Since
-header is set.
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.
Question
Does Spring handle the combination of If-Modified-Since
and If-None-Match
correctly, or could this be a bug?
Update 1
(Thanks Kevin)
It seems to be something wrong on my end, since the implementation details locally and in the spring-project differs.
- I mismatched the main and 5.3.x branch. The local implementation matches the implementation on the 5.3.x branch.
Update 2
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
. This does not seem to be handled by the ShallowEtagHeaderFilter where isEligbleForEtag returns 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.
Update 3
Update 4
I've added a simple reproducer here. It's a clean Spring starter with the Spring Web dependency, a HTML file and the shallow etag filter enabled.
Opening http://localhost:8080/index.html should provide necessary values from ETag
and Last-Modified
in the response headers.
Expected behaviour
Sending a curl request with the ETag
value set as If-None-Match
, and Last-Modified
set as If-Modified-Since
returns 304 Not Modified
as expected.
If you modify If-Modified-Since
to be back in time, you receive a 200 OK
as expected.
Unexpected behaviour
However, if you only modify the ETag
value, meaning that the ETag of the cached content differs from the ETag calculated on the server, you expect to get 200 OK
. But, you get a 304 Not Modified
which is unexpected since If-None-Match
has precedence when If-None-Match
is used in combination with If-Modified-Since
. The precedence is documented in https://tools.ietf.org/html/rfc7232#section-6 referenced in checkNotModified and seemingly obsoleted by https://www.rfc-editor.org/rfc/rfc9110#section-13.2.2.
Curl
(Remember to replace header values)
curl --location --request GET 'http://localhost:8080/index.html' \
--header 'If-None-Match: "0c01d44fcb5bdfc18313ea8cb309217af"' \
--header 'If-Modified-Since: Sat, 27 Aug 2022 23:26:55 GMT'
Problem
The core problem seems to be that checkNotModified
in org.springframework.web.filter.ShallowEtagHeaderFilter
is called before the response is updated with status NOT MODIFIED
when checkNotModified
is called from handleRequest
in org.springframework.web.servlet.resource.ResourceHttpRequestHandler
.