Skip to content

Support ETag generation on ResourceHttpRequestHandler #29031

Closed
@skjelmo

Description

@skjelmo

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 when ShallowEtagHeaderFilter is executed before ResourceHttpRequestHandler which updates the response status code to 304 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.

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions