diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 52646ced870..0e070c3c806 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -214,12 +214,12 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ // Evaluate conditions in order of precedence. // See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2 if (validateIfMatch(eTag)) { - updateResponseStateChanging(); + updateResponseStateChanging(eTag, lastModifiedTimestamp); return this.notModified; } // 2) If-Unmodified-Since else if (validateIfUnmodifiedSince(lastModifiedTimestamp)) { - updateResponseStateChanging(); + updateResponseStateChanging(eTag, lastModifiedTimestamp); return this.notModified; } // 3) If-None-Match @@ -309,10 +309,13 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return first.equals(second); } - private void updateResponseStateChanging() { + private void updateResponseStateChanging(String eTag, long lastModifiedTimestamp) { if (this.notModified && getResponse() != null) { getResponse().setStatus(HttpStatus.PRECONDITION_FAILED.value()); } + else { + addCachingResponseHeaders(eTag, lastModifiedTimestamp); + } } private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) { @@ -347,13 +350,17 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ getResponse().setStatus(isHttpGetOrHead ? HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value()); } - if (isHttpGetOrHead) { - if (lastModifiedTimestamp > 0 && parseDateValue(getResponse().getHeader(HttpHeaders.LAST_MODIFIED)) == -1) { - getResponse().setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp); - } - if (StringUtils.hasLength(eTag) && getResponse().getHeader(HttpHeaders.ETAG) == null) { - getResponse().setHeader(HttpHeaders.ETAG, padEtagIfNecessary(eTag)); - } + addCachingResponseHeaders(eTag, lastModifiedTimestamp); + } + } + + private void addCachingResponseHeaders(String eTag, long lastModifiedTimestamp) { + if (SAFE_METHODS.contains(getRequest().getMethod())) { + if (lastModifiedTimestamp > 0 && parseDateValue(getResponse().getHeader(HttpHeaders.LAST_MODIFIED)) == -1) { + getResponse().setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp); + } + if (StringUtils.hasLength(eTag) && getResponse().getHeader(HttpHeaders.ETAG) == null) { + getResponse().setHeader(HttpHeaders.ETAG, padEtagIfNecessary(eTag)); } } } diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index f43036302a9..06b949f955e 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -259,12 +259,12 @@ public class DefaultServerWebExchange implements ServerWebExchange { // See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2 // 1) If-Match if (validateIfMatch(eTag)) { - updateResponseStateChanging(); + updateResponseStateChanging(eTag, lastModified); return this.notModified; } // 2) If-Unmodified-Since else if (validateIfUnmodifiedSince(lastModified)) { - updateResponseStateChanging(); + updateResponseStateChanging(eTag, lastModified); return this.notModified; } // 3) If-None-Match @@ -346,10 +346,13 @@ public class DefaultServerWebExchange implements ServerWebExchange { return first.equals(second); } - private void updateResponseStateChanging() { + private void updateResponseStateChanging(String eTag, Instant lastModified) { if (this.notModified) { getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED); } + else { + addCachingResponseHeaders(eTag, lastModified); + } } private boolean validateIfNoneMatch(@Nullable String eTag) { @@ -371,7 +374,11 @@ public class DefaultServerWebExchange implements ServerWebExchange { getResponse().setStatusCode(isSafeMethod ? HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED); } - if (isSafeMethod) { + addCachingResponseHeaders(eTag, lastModified); + } + + private void addCachingResponseHeaders(@Nullable String eTag, Instant lastModified) { + if (SAFE_METHODS.contains(getRequest().getMethod())) { if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) { getResponseHeaders().setLastModified(lastModified.toEpochMilli()); } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java index 94447017e91..81cdb8e47f9 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java @@ -126,6 +126,16 @@ class ServletWebRequestHttpMethodsTests { assertPreconditionFailed(); } + @SafeHttpMethodsTest + void ifUnModifiedSinceShouldSetHeadersWithSafeMethod(String method) { + setUpRequest(method); + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + servletRequest.addHeader(HttpHeaders.IF_UNMODIFIED_SINCE, now.toEpochMilli()); + assertThat(request.checkNotModified(oneMinuteAgo.toEpochMilli())).isFalse(); + assertOkWithLastModified(oneMinuteAgo); + } + @SafeHttpMethodsTest void ifNoneMatchShouldMatchIdenticalETagValue(String method) { setUpRequest(method); diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java index 97b963bd32f..2ab3a3a43c9 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -307,4 +307,18 @@ class DefaultServerWebExchangeCheckNotModifiedTests { assertThat(exchange.getResponse().getHeaders().getLastModified()).isEqualTo(-1); } + @Test + void checkNotModifiedTimestampConditionalWithSafeMethod() throws Exception { + String eTag = "\"Foo\""; + Instant oneMinuteAgo = currentDate.minusSeconds(60); + MockServerHttpRequest request = MockServerHttpRequest.get("/") + .ifUnmodifiedSince(currentDate.toEpochMilli()).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertThat(exchange.checkNotModified(eTag, oneMinuteAgo)).isFalse(); + assertThat(exchange.getResponse().getStatusCode()).isNull(); + assertThat(exchange.getResponse().getHeaders().getLastModified()).isEqualTo(oneMinuteAgo.toEpochMilli()); + assertThat(exchange.getResponse().getHeaders().getETag()).isEqualTo(eTag); + } + + }