Add caching headers to If-Unmodified-Since responses
Conditional requests using "If-Unmodified-Since" headers are generally used as precondition checks for state-changing methods (POST, PUT, DELETE). See https://datatracker.ietf.org/doc/html/rfc7232#section-3.4 The spec also allows for idempotent methods like GET and HEAD. Prior to this commit, the "If-Unmodified-Since" processing done in `checkNotModified` (see `ServletWebRequest` and `DefaultServerWebExchange`) would only focus on the state changing methods and not take into account the safe methods. For those cases, the "ETag" and "Last-Modified" would be missing from the response. This commit ensures that such headers are added as expected in these cases. Fixes gh-29362
This commit is contained in:
parent
12cc8a9f07
commit
9410998897
|
@ -220,6 +220,12 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
|||
if (this.notModified && response != null) {
|
||||
response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
|
||||
}
|
||||
if (SAFE_METHODS.contains(getRequest().getMethod())) {
|
||||
if (StringUtils.hasLength(etag) && response.getHeader(HttpHeaders.ETAG) == null) {
|
||||
response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag));
|
||||
}
|
||||
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp);
|
||||
}
|
||||
return this.notModified;
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import org.springframework.web.server.session.WebSessionManager;
|
|||
* Default implementation of {@link ServerWebExchange}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public class DefaultServerWebExchange implements ServerWebExchange {
|
||||
|
@ -261,6 +262,12 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
|||
if (this.notModified) {
|
||||
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
if (SAFE_METHODS.contains(getRequest().getMethod())) {
|
||||
if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) {
|
||||
getResponseHeaders().setETag(padEtagIfNecessary(etag));
|
||||
}
|
||||
getResponseHeaders().setLastModified(lastModified.toEpochMilli());
|
||||
}
|
||||
return this.notModified;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.lang.annotation.Target;
|
|||
import java.time.ZonedDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
|
@ -286,9 +287,9 @@ class ServletWebRequestHttpMethodsTests {
|
|||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
||||
}
|
||||
|
||||
@ParameterizedHttpMethodTest
|
||||
void checkNotModifiedTimestampWithLengthPart(String method) {
|
||||
setUpRequest(method);
|
||||
@Test
|
||||
void checkNotModifiedTimestampWithLengthPart() {
|
||||
setUpRequest("GET");
|
||||
|
||||
long epochTime = ZonedDateTime.parse(CURRENT_TIME, RFC_1123_DATE_TIME).toInstant().toEpochMilli();
|
||||
servletRequest.setMethod("GET");
|
||||
|
@ -299,12 +300,11 @@ class ServletWebRequestHttpMethodsTests {
|
|||
assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000);
|
||||
}
|
||||
|
||||
@ParameterizedHttpMethodTest
|
||||
void checkModifiedTimestampWithLengthPart(String method) {
|
||||
setUpRequest(method);
|
||||
@Test
|
||||
void checkModifiedTimestampWithLengthPart() {
|
||||
setUpRequest("GET");
|
||||
|
||||
long epochTime = ZonedDateTime.parse(CURRENT_TIME, RFC_1123_DATE_TIME).toInstant().toEpochMilli();
|
||||
servletRequest.setMethod("GET");
|
||||
servletRequest.addHeader("If-Modified-Since", "Wed, 08 Apr 2014 09:57:42 GMT; length=13774");
|
||||
|
||||
assertThat(request.checkNotModified(epochTime)).isFalse();
|
||||
|
@ -312,13 +312,12 @@ class ServletWebRequestHttpMethodsTests {
|
|||
assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000);
|
||||
}
|
||||
|
||||
@ParameterizedHttpMethodTest
|
||||
void checkNotModifiedTimestampConditionalPut(String method) {
|
||||
setUpRequest(method);
|
||||
@Test
|
||||
void checkNotModifiedTimestampConditionalPut() {
|
||||
setUpRequest("PUT");
|
||||
|
||||
long currentEpoch = currentDate.getTime();
|
||||
long oneMinuteAgo = currentEpoch - (1000 * 60);
|
||||
servletRequest.setMethod("PUT");
|
||||
servletRequest.addHeader("If-UnModified-Since", currentEpoch);
|
||||
|
||||
assertThat(request.checkNotModified(oneMinuteAgo)).isFalse();
|
||||
|
@ -326,13 +325,12 @@ class ServletWebRequestHttpMethodsTests {
|
|||
assertThat(servletResponse.getHeader("Last-Modified")).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedHttpMethodTest
|
||||
void checkNotModifiedTimestampConditionalPutConflict(String method) {
|
||||
setUpRequest(method);
|
||||
@Test
|
||||
void checkNotModifiedTimestampConditionalPutConflict() {
|
||||
setUpRequest("PUT");
|
||||
|
||||
long currentEpoch = currentDate.getTime();
|
||||
long oneMinuteAgo = currentEpoch - (1000 * 60);
|
||||
servletRequest.setMethod("PUT");
|
||||
servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo);
|
||||
|
||||
assertThat(request.checkNotModified(currentEpoch)).isTrue();
|
||||
|
@ -340,6 +338,19 @@ class ServletWebRequestHttpMethodsTests {
|
|||
assertThat(servletResponse.getHeader("Last-Modified")).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedHttpMethodTest
|
||||
void checkNotModifiedConditionalGet(String method) {
|
||||
setUpRequest(method);
|
||||
long currentEpoch = currentDate.getTime();
|
||||
long oneMinuteAgo = currentEpoch - (1000 * 60);
|
||||
servletRequest.addHeader("If-UnModified-Since", currentEpoch);
|
||||
|
||||
assertThat(request.checkNotModified(oneMinuteAgo)).isFalse();
|
||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
||||
assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(oneMinuteAgo / 1000);
|
||||
}
|
||||
|
||||
|
||||
private void setUpRequest(String method) {
|
||||
this.servletRequest.setMethod(method);
|
||||
this.servletRequest.setRequestURI("https://example.org");
|
||||
|
|
|
@ -307,4 +307,17 @@ class DefaultServerWebExchangeCheckNotModifiedTests {
|
|||
assertThat(exchange.getResponse().getHeaders().getLastModified()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkNotModifiedTimestampConditionalGet() throws Exception {
|
||||
String eTag = "\"Test\"";
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue