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 946a19ad60..52646ced87 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -206,45 +206,113 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ } @Override - public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) { + 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()); - } + // See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2 + if (validateIfMatch(eTag)) { + updateResponseStateChanging(); return this.notModified; } - - boolean validated = validateIfNoneMatch(etag); - if (!validated) { + // 2) If-Unmodified-Since + else if (validateIfUnmodifiedSince(lastModifiedTimestamp)) { + updateResponseStateChanging(); + return this.notModified; + } + // 3) If-None-Match + if (!validateIfNoneMatch(eTag)) { + // 4) If-Modified-Since validateIfModifiedSince(lastModifiedTimestamp); } + updateResponseIdempotent(eTag, lastModifiedTimestamp); + return this.notModified; + } - // 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); + private boolean validateIfMatch(@Nullable String eTag) { + Enumeration ifMatchHeaders = getRequest().getHeaders(HttpHeaders.IF_MATCH); + if (SAFE_METHODS.contains(getRequest().getMethod())) { + return false; + } + if (!ifMatchHeaders.hasMoreElements()) { + return false; + } + this.notModified = matchRequestedETags(ifMatchHeaders, eTag, false); + return true; + } + + private boolean validateIfNoneMatch(@Nullable String eTag) { + Enumeration ifNoneMatchHeaders = getRequest().getHeaders(HttpHeaders.IF_NONE_MATCH); + if (!ifNoneMatchHeaders.hasMoreElements()) { + return false; + } + this.notModified = !matchRequestedETags(ifNoneMatchHeaders, eTag, true); + return true; + } + + private boolean matchRequestedETags(Enumeration requestedETags, @Nullable String eTag, boolean weakCompare) { + eTag = padEtagIfNecessary(eTag); + while (requestedETags.hasMoreElements()) { + // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 + Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(requestedETags.nextElement()); + while (eTagMatcher.find()) { + // only consider "lost updates" checks for unsafe HTTP methods + if ("*".equals(eTagMatcher.group()) && StringUtils.hasLength(eTag) + && !SAFE_METHODS.contains(getRequest().getMethod())) { + return false; } - if (StringUtils.hasLength(etag) && response.getHeader(HttpHeaders.ETAG) == null) { - response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag)); + if (weakCompare) { + if (eTagWeakMatch(eTag, eTagMatcher.group(1))) { + return false; + } + } + else { + if (eTagStrongMatch(eTag, eTagMatcher.group(1))) { + return false; + } } } } + return true; + } - return this.notModified; + @Nullable + private String padEtagIfNecessary(@Nullable String etag) { + if (!StringUtils.hasLength(etag)) { + return etag; + } + if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) { + return etag; + } + return "\"" + etag + "\""; + } + + private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) { + if (!StringUtils.hasLength(first) || first.startsWith("W/")) { + return false; + } + return first.equals(second); + } + + private boolean eTagWeakMatch(@Nullable String first, @Nullable String second) { + if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) { + return false; + } + if (first.startsWith("W/")) { + first = first.substring(2); + } + if (second.startsWith("W/")) { + second = second.substring(2); + } + return first.equals(second); + } + + private void updateResponseStateChanging() { + if (this.notModified && getResponse() != null) { + getResponse().setStatus(HttpStatus.PRECONDITION_FAILED.value()); + } } private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) { @@ -255,57 +323,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ if (ifUnmodifiedSince == -1) { return false; } - // We will perform this validation... this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000)); return true; } - private boolean validateIfNoneMatch(@Nullable String etag) { - if (!StringUtils.hasLength(etag)) { - return false; - } - - Enumeration ifNoneMatch; - try { - ifNoneMatch = getRequest().getHeaders(HttpHeaders.IF_NONE_MATCH); - } - catch (IllegalArgumentException ex) { - return false; - } - if (!ifNoneMatch.hasMoreElements()) { - return false; - } - - // We will perform this validation... - etag = padEtagIfNecessary(etag); - if (etag.startsWith("W/")) { - etag = etag.substring(2); - } - while (ifNoneMatch.hasMoreElements()) { - String clientETags = ifNoneMatch.nextElement(); - Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags); - // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 - while (etagMatcher.find()) { - if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) { - this.notModified = true; - break; - } - } - } - - return true; - } - - private String padEtagIfNecessary(String etag) { - if (!StringUtils.hasLength(etag)) { - return etag; - } - if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) { - return etag; - } - return "\"" + etag + "\""; - } - private boolean validateIfModifiedSince(long lastModifiedTimestamp) { if (lastModifiedTimestamp < 0) { return false; @@ -319,6 +340,24 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return true; } + private void updateResponseIdempotent(String eTag, long lastModifiedTimestamp) { + if (getResponse() != null) { + boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod()); + if (this.notModified) { + 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)); + } + } + } + } + public boolean isNotModified() { return this.notModified; } 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 621893f4cf..f43036302a 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 @@ -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. @@ -57,6 +57,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 { @@ -249,44 +250,135 @@ public class DefaultServerWebExchange implements ServerWebExchange { } @Override - public boolean checkNotModified(@Nullable String etag, Instant lastModified) { + public boolean checkNotModified(@Nullable String eTag, Instant lastModified) { HttpStatusCode status = getResponse().getStatusCode(); if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) { return this.notModified; } - // Evaluate conditions in order of precedence. - // See https://tools.ietf.org/html/rfc7232#section-6 - - if (validateIfUnmodifiedSince(lastModified)) { - if (this.notModified) { - getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED); - } + // See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2 + // 1) If-Match + if (validateIfMatch(eTag)) { + updateResponseStateChanging(); return this.notModified; } - - boolean validated = validateIfNoneMatch(etag); - if (!validated) { + // 2) If-Unmodified-Since + else if (validateIfUnmodifiedSince(lastModified)) { + updateResponseStateChanging(); + return this.notModified; + } + // 3) If-None-Match + if (!validateIfNoneMatch(eTag)) { + // 4) If-Modified-Since validateIfModifiedSince(lastModified); } + updateResponseIdempotent(eTag, lastModified); + return this.notModified; + } - // Update response + private boolean validateIfMatch(@Nullable String eTag) { + try { + if (SAFE_METHODS.contains(getRequest().getMethod())) { + return false; + } + if (CollectionUtils.isEmpty(getRequest().getHeaders().get(HttpHeaders.IF_MATCH))) { + return false; + } + this.notModified = matchRequestedETags(getRequestHeaders().getIfMatch(), eTag, false); + } + catch (IllegalArgumentException ex) { + return false; + } + return true; + } - boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod()); + private boolean matchRequestedETags(List requestedETags, @Nullable String eTag, boolean weakCompare) { + eTag = padEtagIfNecessary(eTag); + for (String clientEtag : requestedETags) { + // only consider "lost updates" checks for unsafe HTTP methods + if ("*".equals(clientEtag) && StringUtils.hasLength(eTag) + && !SAFE_METHODS.contains(getRequest().getMethod())) { + return false; + } + // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 + if (weakCompare) { + if (eTagWeakMatch(eTag, clientEtag)) { + return false; + } + } + else { + if (eTagStrongMatch(eTag, clientEtag)) { + return false; + } + } + } + return true; + } + + @Nullable + private String padEtagIfNecessary(@Nullable String etag) { + if (!StringUtils.hasLength(etag)) { + return etag; + } + if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) { + return etag; + } + return "\"" + etag + "\""; + } + + private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) { + if (!StringUtils.hasLength(first) || first.startsWith("W/")) { + return false; + } + return first.equals(second); + } + + private boolean eTagWeakMatch(@Nullable String first, @Nullable String second) { + if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) { + return false; + } + if (first.startsWith("W/")) { + first = first.substring(2); + } + if (second.startsWith("W/")) { + second = second.substring(2); + } + return first.equals(second); + } + + private void updateResponseStateChanging() { if (this.notModified) { - getResponse().setStatusCode(isHttpGetOrHead ? + getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED); + } + } + + private boolean validateIfNoneMatch(@Nullable String eTag) { + try { + if (CollectionUtils.isEmpty(getRequest().getHeaders().get(HttpHeaders.IF_NONE_MATCH))) { + return false; + } + this.notModified = !matchRequestedETags(getRequestHeaders().getIfNoneMatch(), eTag, true); + } + catch (IllegalArgumentException ex) { + return false; + } + return true; + } + + private void updateResponseIdempotent(@Nullable String eTag, Instant lastModified) { + boolean isSafeMethod = SAFE_METHODS.contains(getRequest().getMethod()); + if (this.notModified) { + getResponse().setStatusCode(isSafeMethod ? HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED); } - if (isHttpGetOrHead) { + if (isSafeMethod) { if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) { getResponseHeaders().setLastModified(lastModified.toEpochMilli()); } - if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) { - getResponseHeaders().setETag(padEtagIfNecessary(etag)); + if (StringUtils.hasLength(eTag) && getResponseHeaders().getETag() == null) { + getResponseHeaders().setETag(padEtagIfNecessary(eTag)); } } - - return this.notModified; } private boolean validateIfUnmodifiedSince(Instant lastModified) { @@ -297,56 +389,11 @@ public class DefaultServerWebExchange implements ServerWebExchange { if (ifUnmodifiedSince == -1) { return false; } - // We will perform this validation... Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince); this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS)); return true; } - private boolean validateIfNoneMatch(@Nullable String etag) { - if (!StringUtils.hasLength(etag)) { - return false; - } - List ifNoneMatch; - try { - ifNoneMatch = getRequestHeaders().getIfNoneMatch(); - } - catch (IllegalArgumentException ex) { - return false; - } - if (ifNoneMatch.isEmpty()) { - return false; - } - // We will perform this validation... - etag = padEtagIfNecessary(etag); - if (etag.startsWith("W/")) { - etag = etag.substring(2); - } - for (String clientEtag : ifNoneMatch) { - // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 - if (StringUtils.hasLength(clientEtag)) { - if (clientEtag.startsWith("W/")) { - clientEtag = clientEtag.substring(2); - } - if (clientEtag.equals(etag)) { - this.notModified = true; - break; - } - } - } - return true; - } - - private String padEtagIfNecessary(String etag) { - if (!StringUtils.hasLength(etag)) { - return etag; - } - if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) { - return etag; - } - return "\"" + etag + "\""; - } - private boolean validateIfModifiedSince(Instant lastModified) { if (lastModified.isBefore(Instant.EPOCH)) { return false; 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 27a42f96f7..94447017e9 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 @@ -20,12 +20,17 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.time.Instant; import java.time.ZonedDateTime; -import java.util.Date; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -44,313 +49,272 @@ class ServletWebRequestHttpMethodsTests { private static final String CURRENT_TIME = "Wed, 9 Apr 2014 09:57:42 GMT"; + private static final Instant NOW = Instant.now(); + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest(); private final MockHttpServletResponse servletResponse = new MockHttpServletResponse(); private final ServletWebRequest request = new ServletWebRequest(servletRequest, servletResponse); - private final Date currentDate = new Date(); - - @ParameterizedHttpMethodTest - void checkNotModifiedNon2xxStatus(String method) { - setUpRequest(method); - - long epochTime = currentDate.getTime(); - servletRequest.addHeader("If-Modified-Since", epochTime); - servletResponse.setStatus(304); - - assertThat(request.checkNotModified(epochTime)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("Last-Modified")).isNull(); + @Test + void ifMatchWildcardShouldMatchWhenETagPresent() { + setUpRequest("PUT"); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "*"); + assertThat(request.checkNotModified("\"SomeETag\"")).isFalse(); } - @ParameterizedHttpMethodTest // SPR-13516 - void checkNotModifiedInvalidStatus(String method) { - setUpRequest(method); - - long epochTime = currentDate.getTime(); - servletRequest.addHeader("If-Modified-Since", epochTime); - servletResponse.setStatus(0); - - assertThat(request.checkNotModified(epochTime)).isFalse(); + @Test + void ifMatchWildcardShouldMatchETagMissing() { + setUpRequest("PUT"); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "*"); + assertThat(request.checkNotModified("")).isTrue(); + assertPreconditionFailed(); } - @ParameterizedHttpMethodTest // SPR-14559 - void checkNotModifiedInvalidIfNoneMatchHeader(String method) { - setUpRequest(method); + @Test + void ifMatchValueShouldMatchWhenETagMatches() { + setUpRequest("PUT"); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "\"first\""); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "\"second\""); + assertThat(request.checkNotModified("\"second\"")).isFalse(); + } + @Test + void ifMatchValueShouldRejectWhenETagDoesNotMatch() { + setUpRequest("PUT"); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "\"first\""); + assertThat(request.checkNotModified("\"second\"")).isTrue(); + assertPreconditionFailed(); + } + + @Test + void ifMatchValueShouldUseStrongComparison() { + setUpRequest("PUT"); + String eTag = "\"spring\""; + servletRequest.addHeader(HttpHeaders.IF_MATCH, "W/" + eTag); + assertThat(request.checkNotModified(eTag)).isTrue(); + assertPreconditionFailed(); + } + + @SafeHttpMethodsTest + void ifMatchShouldOnlyBeConsideredForUnsafeMethods(String method) { + setUpRequest(method); + servletRequest.addHeader(HttpHeaders.IF_MATCH, "*"); + assertThat(request.checkNotModified("\"spring\"")).isFalse(); + } + + @Test + void ifUnModifiedSinceShouldMatchValueWhenLater() { + setUpRequest("PUT"); + 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(); + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)).isNull(); + } + + @Test + void ifUnModifiedSinceShouldNotMatchValueWhenEarlier() { + setUpRequest("PUT"); + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + servletRequest.addHeader(HttpHeaders.IF_UNMODIFIED_SINCE, oneMinuteAgo.toEpochMilli()); + assertThat(request.checkNotModified(now.toEpochMilli())).isTrue(); + assertPreconditionFailed(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchIdenticalETagValue(String method) { + setUpRequest(method); + String etag = "\"spring\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag); + assertThat(request.checkNotModified(etag)).isTrue(); + assertNotModified(etag, null); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchETagWithSeparatorChar(String method) { + setUpRequest(method); + String etag = "\"spring,framework\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag); + assertThat(request.checkNotModified(etag)).isTrue(); + assertNotModified(etag, null); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldNotMatchDifferentETag(String method) { + setUpRequest(method); + String etag = "\"framework\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "\"spring\""); + assertThat(request.checkNotModified(etag)).isFalse(); + assertOkWithETag(etag); + } + + @SafeHttpMethodsTest + // SPR-14559 + void ifNoneMatchShouldNotFailForUnquotedETag(String method) { + setUpRequest(method); String etag = "\"etagvalue\""; - servletRequest.addHeader("If-None-Match", "missingquotes"); + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "missingquotes"); assertThat(request.checkNotModified(etag)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); + assertOkWithETag(etag); } - @ParameterizedHttpMethodTest - void checkNotModifiedHeaderAlreadySet(String method) { + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchPaddedETag(String method) { setUpRequest(method); - - long epochTime = currentDate.getTime(); - servletRequest.addHeader("If-Modified-Since", epochTime); - servletResponse.addHeader("Last-Modified", CURRENT_TIME); - - assertThat(request.checkNotModified(epochTime)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeaders("Last-Modified").size()).isEqualTo(1); - assertThat(servletResponse.getHeader("Last-Modified")).isEqualTo(CURRENT_TIME); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedTimestamp(String method) { - setUpRequest(method); - - long epochTime = currentDate.getTime(); - servletRequest.addHeader("If-Modified-Since", epochTime); - - assertThat(request.checkNotModified(epochTime)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(currentDate.getTime() / 1000); - } - - @ParameterizedHttpMethodTest - void checkModifiedTimestamp(String method) { - setUpRequest(method); - - long oneMinuteAgo = currentDate.getTime() - (1000 * 60); - servletRequest.addHeader("If-Modified-Since", oneMinuteAgo); - - assertThat(request.checkNotModified(currentDate.getTime())).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(currentDate.getTime() / 1000); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedETag(String method) { - setUpRequest(method); - - String etag = "\"Foo\""; - servletRequest.addHeader("If-None-Match", etag); - + String etag = "spring"; + String paddedEtag = String.format("\"%s\"", etag); + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, paddedEtag); assertThat(request.checkNotModified(etag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); + assertNotModified(paddedEtag, null); } - @ParameterizedHttpMethodTest - void checkNotModifiedETagWithSeparatorChars(String method) { + @SafeHttpMethodsTest + void ifNoneMatchShouldIgnoreWildcard(String method) { setUpRequest(method); - - String etag = "\"Foo, Bar\""; - servletRequest.addHeader("If-None-Match", etag); - - assertThat(request.checkNotModified(etag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); - } - - - @ParameterizedHttpMethodTest - void checkModifiedETag(String method) { - setUpRequest(method); - - String currentETag = "\"Foo\""; - String oldETag = "Bar"; - servletRequest.addHeader("If-None-Match", oldETag); - - assertThat(request.checkNotModified(currentETag)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(currentETag); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedUnpaddedETag(String method) { - setUpRequest(method); - - String etag = "Foo"; - String paddedETag = String.format("\"%s\"", etag); - servletRequest.addHeader("If-None-Match", paddedETag); - - assertThat(request.checkNotModified(etag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(paddedETag); - } - - @ParameterizedHttpMethodTest - void checkModifiedUnpaddedETag(String method) { - setUpRequest(method); - - String currentETag = "Foo"; - String oldETag = "Bar"; - servletRequest.addHeader("If-None-Match", oldETag); - - assertThat(request.checkNotModified(currentETag)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(String.format("\"%s\"", currentETag)); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedWildcardIsIgnored(String method) { - setUpRequest(method); - - String etag = "\"Foo\""; - servletRequest.addHeader("If-None-Match", "*"); - + String etag = "\"spring\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "*"); assertThat(request.checkNotModified(etag)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); + assertOkWithETag(etag); } - @ParameterizedHttpMethodTest - void checkNotModifiedETagAndTimestamp(String method) { - setUpRequest(method); - - String etag = "\"Foo\""; - servletRequest.addHeader("If-None-Match", etag); - servletRequest.addHeader("If-Modified-Since", currentDate.getTime()); - - assertThat(request.checkNotModified(etag, currentDate.getTime())).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(currentDate.getTime() / 1000); + @Test + void ifNoneMatchShouldRejectWildcardForUnsafeMethods() { + setUpRequest("PUT"); + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "*"); + assertThat(request.checkNotModified("\"spring\"")).isTrue(); + assertPreconditionFailed(); } - @ParameterizedHttpMethodTest // SPR-14224 - void checkNotModifiedETagAndModifiedTimestamp(String method) { + @SafeHttpMethodsTest + void ifNoneMatchValueShouldUseWeakComparison(String method) { setUpRequest(method); - - String etag = "\"Foo\""; - servletRequest.addHeader("If-None-Match", etag); - long currentEpoch = currentDate.getTime(); - long oneMinuteAgo = currentEpoch - (1000 * 60); - servletRequest.addHeader("If-Modified-Since", oneMinuteAgo); - - assertThat(request.checkNotModified(etag, currentEpoch)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(currentDate.getTime() / 1000); - } - - @ParameterizedHttpMethodTest - void checkModifiedETagAndNotModifiedTimestamp(String method) { - setUpRequest(method); - - String currentETag = "\"Foo\""; - String oldETag = "\"Bar\""; - servletRequest.addHeader("If-None-Match", oldETag); - long epochTime = currentDate.getTime(); - servletRequest.addHeader("If-Modified-Since", epochTime); - - assertThat(request.checkNotModified(currentETag, epochTime)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(currentETag); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(currentDate.getTime() / 1000); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedETagWeakStrong(String method) { - setUpRequest(method); - - String etag = "\"Foo\""; - String weakETag = String.format("W/%s", etag); - servletRequest.addHeader("If-None-Match", etag); - - assertThat(request.checkNotModified(weakETag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(weakETag); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedETagStrongWeak(String method) { - setUpRequest(method); - - String etag = "\"Foo\""; - servletRequest.addHeader("If-None-Match", String.format("W/%s", etag)); - + String etag = "\"spring\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "W/" + etag); assertThat(request.checkNotModified(etag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); + assertNotModified(etag, null); } - @ParameterizedHttpMethodTest - void checkNotModifiedMultipleETags(String method) { + @SafeHttpMethodsTest + void ifModifiedSinceShouldMatchIfDatesEqual(String method) { setUpRequest(method); - - String etag = "\"Bar\""; - String multipleETags = String.format("\"Foo\", %s", etag); - servletRequest.addHeader("If-None-Match", multipleETags); - - assertThat(request.checkNotModified(etag)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli()); + assertThat(request.checkNotModified(NOW.toEpochMilli())).isTrue(); + assertNotModified(null, NOW); } - @ParameterizedHttpMethodTest - void checkNotModifiedTimestampWithLengthPart(String method) { + @SafeHttpMethodsTest + void ifModifiedSinceShouldNotMatchIfDateAfter(String method) { setUpRequest(method); + Instant oneMinuteLater = NOW.plus(1, ChronoUnit.MINUTES); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli()); + assertThat(request.checkNotModified(oneMinuteLater.toEpochMilli())).isFalse(); + assertOkWithLastModified(oneMinuteLater); + } + @SafeHttpMethodsTest + void ifModifiedSinceShouldNotOverrideResponseStatus(String method) { + setUpRequest(method); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli()); + servletResponse.setStatus(304); + assertThat(request.checkNotModified(NOW.toEpochMilli())).isFalse(); + assertNotModified(null, null); + } + + @SafeHttpMethodsTest + // SPR-13516 + void ifModifiedSinceShouldNotFailForInvalidResponseStatus(String method) { + setUpRequest(method); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli()); + servletResponse.setStatus(0); + assertThat(request.checkNotModified(NOW.toEpochMilli())).isFalse(); + } + + @SafeHttpMethodsTest + void ifModifiedSinceShouldNotFailForTimestampWithLengthPart(String method) { + setUpRequest(method); long epochTime = ZonedDateTime.parse(CURRENT_TIME, RFC_1123_DATE_TIME).toInstant().toEpochMilli(); - servletRequest.setMethod("GET"); - servletRequest.addHeader("If-Modified-Since", "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"); assertThat(request.checkNotModified(epochTime)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(304); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000); + assertNotModified(null, Instant.ofEpochMilli(epochTime)); } - @ParameterizedHttpMethodTest - void checkModifiedTimestampWithLengthPart(String method) { + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndDate(String method) { setUpRequest(method); - - 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(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000); + String etag = "\"spring\""; + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli()); + assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isTrue(); + assertNotModified(etag, NOW); } - @ParameterizedHttpMethodTest - void checkNotModifiedTimestampConditionalPut(String method) { + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndLaterDate(String method) { setUpRequest(method); - - long currentEpoch = currentDate.getTime(); - long oneMinuteAgo = currentEpoch - (1000 * 60); - servletRequest.setMethod("PUT"); - servletRequest.addHeader("If-UnModified-Since", currentEpoch); - - assertThat(request.checkNotModified(oneMinuteAgo)).isFalse(); - assertThat(servletResponse.getStatus()).isEqualTo(200); - assertThat(servletResponse.getHeader("Last-Modified")).isNull(); + String etag = "\"spring\""; + Instant oneMinuteLater = NOW.plus(1, ChronoUnit.MINUTES); + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, oneMinuteLater.toEpochMilli()); + assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isTrue(); + assertNotModified(etag, NOW); } - @ParameterizedHttpMethodTest - void checkNotModifiedTimestampConditionalPutConflict(String method) { + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldNotMatchWhenDifferentETag(String method) { setUpRequest(method); - - long currentEpoch = currentDate.getTime(); - long oneMinuteAgo = currentEpoch - (1000 * 60); - servletRequest.setMethod("PUT"); - servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo); - - assertThat(request.checkNotModified(currentEpoch)).isTrue(); - assertThat(servletResponse.getStatus()).isEqualTo(412); - assertThat(servletResponse.getHeader("Last-Modified")).isNull(); + String etag = "\"framework\""; + Instant oneMinuteLater = NOW.plus(1, ChronoUnit.MINUTES); + servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "\"spring\""); + servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, oneMinuteLater.toEpochMilli()); + assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isFalse(); + assertOkWithETag(etag); + assertOkWithLastModified(NOW); } + private void setUpRequest(String method) { this.servletRequest.setMethod(method); this.servletRequest.setRequestURI("https://example.org"); } + private void assertPreconditionFailed() { + assertThat(this.servletResponse.getStatus()).isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); + } + + private void assertNotModified(@Nullable String eTag, @Nullable Instant lastModified) { + assertThat(this.servletResponse.getStatus()).isEqualTo(HttpStatus.NOT_MODIFIED.value()); + if (eTag != null) { + assertThat(servletResponse.getHeader(HttpHeaders.ETAG)).isEqualTo(eTag); + } + if (lastModified != null) { + assertThat(servletResponse.getDateHeader(HttpHeaders.LAST_MODIFIED) / 1000) + .isEqualTo(lastModified.toEpochMilli() / 1000); + } + } + + private void assertOkWithETag(String eTag) { + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getHeader(HttpHeaders.ETAG)).isEqualTo(eTag); + } + + private void assertOkWithLastModified(Instant lastModified) { + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getDateHeader(HttpHeaders.LAST_MODIFIED) / 1000) + .isEqualTo(lastModified.toEpochMilli() / 1000); + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @ParameterizedTest(name = "[{index}] {0}") - @ValueSource(strings = { "GET", "HEAD" }) - @interface ParameterizedHttpMethodTest { + @ValueSource(strings = {"GET", "HEAD"}) + @interface SafeHttpMethodsTest { } } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 445aa47e7b..f2685fcff1 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -30,6 +30,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link ShallowEtagHeaderFilter}. * @author Arjen Poutsma * @author Brian Clozel * @author Juergen Hoeller diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java index 5a28bd98b1..90313b59b2 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java @@ -360,6 +360,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { */ B ifUnmodifiedSince(long ifUnmodifiedSince); + /** + * Set the values of the {@code If-Match} header. + * @param ifMatches the new value of the header + */ + B ifMatch(String... ifMatches); + /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header @@ -556,6 +562,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { return this; } + @Override + public BodyBuilder ifMatch(String... ifMatches) { + this.headers.setIfMatch(Arrays.asList(ifMatches)); + return this; + } + @Override public BodyBuilder ifNoneMatch(String... ifNoneMatches) { this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java index 9bbab1a7ea..0b393f2032 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -352,13 +352,14 @@ public interface ServerRequest { * also with conditional POST/PUT/DELETE requests. *

Note: you can use either * this {@link #checkNotModified(Instant)} method; or - * {@code #checkNotModified(String)}. If you want enforce both + * {@code #checkNotModified(String)}. If you want to enforce both * a strong entity tag and a Last-Modified value, * as recommended by the HTTP specification, * then you should use {@link #checkNotModified(Instant, String)}. * @param etag the entity tag that the application determined * for the underlying resource. This parameter will be padded - * with quotes (") if necessary. + * with quotes (") if necessary. Use an empty string {@code ""} + * for no value. * @return a corresponding response if the request qualifies as not * modified, or an empty result otherwise * @since 5.2.5 @@ -391,7 +392,8 @@ public interface ServerRequest { * application determined for the underlying resource * @param etag the entity tag that the application determined * for the underlying resource. This parameter will be padded - * with quotes (") if necessary. + * with quotes (") if necessary. Use an empty string {@code ""} + * for no value. * @return a corresponding response if the request qualifies as not * modified, or an empty result otherwise. * @since 5.2.5 diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java index 65df3a89b2..0a6d06296b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -34,7 +34,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; +import java.util.function.Consumer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -58,6 +60,7 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.multipart.FormFieldPart; import org.springframework.http.codec.multipart.Part; +import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebInputException; @@ -70,7 +73,10 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException import static org.springframework.web.reactive.function.BodyExtractors.toMono; /** + * Tests for {@link DefaultServerRequest} and {@link ServerRequest}. + * * @author Arjen Poutsma + * @author Brian Clozel */ public class DefaultServerRequestTests { @@ -237,139 +243,148 @@ public class DefaultServerRequestTests { } - @Test - public void body() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); + @Nested + class BodyTests { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); + @Test + public void body() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + Mono resultMono = request.body(toMono(String.class)); + assertThat(resultMono.block()).isEqualTo("foo"); + } + + @Test + public void bodyToMono() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + Mono resultMono = request.bodyToMono(String.class); + assertThat(resultMono.block()).isEqualTo("foo"); + } + + @Test + public void bodyToMonoParameterizedTypeReference() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + ParameterizedTypeReference typeReference = new ParameterizedTypeReference<>() { + }; + Mono resultMono = request.bodyToMono(typeReference); + assertThat(resultMono.block()).isEqualTo("foo"); + } + + @Test + public void bodyToMonoDecodingException() { + byte[] bytes = "{\"invalid\":\"json\" ".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.POST, "https://example.com/invalid") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + Mono> resultMono = request.bodyToMono( + new ParameterizedTypeReference>() { + }); + StepVerifier.create(resultMono) + .expectError(ServerWebInputException.class) + .verify(); + } + + @Test + public void bodyToFlux() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + Flux resultFlux = request.bodyToFlux(String.class); + assertThat(resultFlux.collectList().block()).isEqualTo(Collections.singletonList("foo")); + } + + @Test + public void bodyToFluxParameterizedTypeReference() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); + + ParameterizedTypeReference typeReference = new ParameterizedTypeReference<>() { + }; + Flux resultFlux = request.bodyToFlux(typeReference); + assertThat(resultFlux.collectList().block()).isEqualTo(Collections.singletonList("foo")); + } + + @Test + public void bodyUnacceptable() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.GET, "https://example.com?foo=bar") + .headers(httpHeaders) + .body(body); + DefaultServerRequest request = createRequest(mockRequest); + + Flux resultFlux = request.bodyToFlux(String.class); + StepVerifier.create(resultFlux) + .expectError(UnsupportedMediaTypeStatusException.class) + .verify(); + } - Mono resultMono = request.body(toMono(String.class)); - assertThat(resultMono.block()).isEqualTo("foo"); } - @Test - public void bodyToMono() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); - - Mono resultMono = request.bodyToMono(String.class); - assertThat(resultMono.block()).isEqualTo("foo"); - } - - @Test - public void bodyToMonoParameterizedTypeReference() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); - - ParameterizedTypeReference typeReference = new ParameterizedTypeReference<>() {}; - Mono resultMono = request.bodyToMono(typeReference); - assertThat(resultMono.block()).isEqualTo("foo"); - } - - @Test - public void bodyToMonoDecodingException() { - byte[] bytes = "{\"invalid\":\"json\" ".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.APPLICATION_JSON); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.POST, "https://example.com/invalid") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); - - Mono> resultMono = request.bodyToMono( - new ParameterizedTypeReference>() {}); - StepVerifier.create(resultMono) - .expectError(ServerWebInputException.class) - .verify(); - } - - @Test - public void bodyToFlux() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); - - Flux resultFlux = request.bodyToFlux(String.class); - assertThat(resultFlux.collectList().block()).isEqualTo(Collections.singletonList("foo")); - } - - @Test - public void bodyToFluxParameterizedTypeReference() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders); - - ParameterizedTypeReference typeReference = new ParameterizedTypeReference<>() {}; - Flux resultFlux = request.bodyToFlux(typeReference); - assertThat(resultFlux.collectList().block()).isEqualTo(Collections.singletonList("foo")); - } - - @Test - public void bodyUnacceptable() { - byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); - DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); - Flux body = Flux.just(dataBuffer); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(MediaType.TEXT_PLAIN); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.GET, "https://example.com?foo=bar") - .headers(httpHeaders) - .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Flux resultFlux = request.bodyToFlux(String.class); - StepVerifier.create(resultFlux) - .expectError(UnsupportedMediaTypeStatusException.class) - .verify(); - } @Test public void formData() { @@ -383,7 +398,7 @@ public class DefaultServerRequestTests { .method(HttpMethod.GET, "https://example.com") .headers(httpHeaders) .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + DefaultServerRequest request = createRequest(mockRequest); Mono> resultData = request.formData(); StepVerifier.create(resultData) @@ -416,7 +431,7 @@ public class DefaultServerRequestTests { .method(HttpMethod.GET, "https://example.com") .headers(httpHeaders) .body(body); - DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + DefaultServerRequest request = createRequest(mockRequest); Mono> resultData = request.multipartData(); StepVerifier.create(resultData) @@ -438,259 +453,293 @@ public class DefaultServerRequestTests { .verifyComplete(); } - @ParameterizedHttpMethodTest - void checkNotModifiedTimestamp(String method) throws Exception { - Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - HttpHeaders headers = new HttpHeaders(); - headers.setIfModifiedSince(now); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(now); - - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); - assertThat(serverResponse.headers().getLastModified()).isEqualTo(now.toEpochMilli()); - }) - .verifyComplete(); + private DefaultServerRequest createRequest(MockServerHttpRequest mockRequest) { + return new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); } - @ParameterizedHttpMethodTest - void checkModifiedTimestamp(String method) { - Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); - HttpHeaders headers = new HttpHeaders(); - headers.setIfModifiedSince(oneMinuteAgo); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + @Nested + class CheckNotModifiedTests { - Mono result = request.checkNotModified(now); + @Test + void ifMatchWildcardShouldMatchWhenETagPresent() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .header(HttpHeaders.IF_MATCH, "*").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"SomeETag\""); - StepVerifier.create(result) - .verifyComplete(); - } + StepVerifier.create(result) + .verifyComplete(); + } - @ParameterizedHttpMethodTest - void checkNotModifiedETag(String method) { - String eTag = "\"Foo\""; - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(eTag); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); + @Test + void ifMatchWildcardShouldMatchWhenETagMissing() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .header(HttpHeaders.IF_MATCH, "*").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(""); - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + StepVerifier.create(result) + .assertNext(assertPreconditionFailed()) + .verifyComplete(); + } - Mono result = request.checkNotModified(eTag); + @Test + void ifMatchValueShouldMatchWhenETagMatches() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .ifMatch("\"first\"", "\"second\"").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"second\""); - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); + StepVerifier.create(result).verifyComplete(); + } + + @Test + void ifMatchValueShouldNotMatchWhenETagDoesNotMatch() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .header(HttpHeaders.IF_MATCH, "\"first\"", "\"second\"").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"third\""); + + StepVerifier.create(result) + .assertNext(assertPreconditionFailed()) + .verifyComplete(); + } + + @Test + void ifMatchValueShouldUseStrongComparison() { + String eTag = "\"spring\""; + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .header(HttpHeaders.IF_MATCH, "W/" + eTag).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(eTag); + + StepVerifier.create(result) + .assertNext(assertPreconditionFailed()) + .verifyComplete(); + } + + @Test + void ifMatchShouldOnlyBeConsideredForUnsafeMethods() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.get("/") + .header(HttpHeaders.IF_MATCH, "*").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"spring\""); + + StepVerifier.create(result).verifyComplete(); + } + + @Test + void ifUnModifiedSinceShouldMatchValueWhenLater() { + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .ifUnmodifiedSince(now.toEpochMilli()).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(oneMinuteAgo); + + StepVerifier.create(result).verifyComplete(); + } + + @Test + void ifUnModifiedSinceShouldNotMatchValueWhenEarlier() { + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .ifUnmodifiedSince(oneMinuteAgo.toEpochMilli()).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now); + + StepVerifier.create(result) + .assertNext(assertPreconditionFailed()) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchIdenticalETagValue(String method) { + String eTag = "\"Foo\""; + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch(eTag).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(eTag, null)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchETagWithSeparatorChar(String method) { + String eTag = "\"Foo, Bar\""; + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch(eTag).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(eTag, null)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldNotMatchDifferentETag(String method) { + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch("Bar").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"Foo\""); + + StepVerifier.create(result).verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldMatchPaddedETag(String method) { + String eTag = "Foo"; + String paddedEtag = String.format("\"%s\"", eTag); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch(paddedEtag).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(paddedEtag, null)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchValueShouldUseWeakComparison(String method) { + String eTag = "\"spring\""; + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch("W/" + eTag).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(eTag, null)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifNoneMatchShouldIgnoreWildcard(String method) { + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch("*").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"spring\""); + StepVerifier.create(result).verifyComplete(); + } + + @Test + void ifNoneMatchShouldRejectWildcardForUnsafeMethods() { + MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/") + .ifNoneMatch("*").build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified("\"spring\""); + StepVerifier.create(result) + .assertNext(assertPreconditionFailed()) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifModifiedSinceShouldMatchIfDatesEqual(String method) { + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifModifiedSince(now.toEpochMilli()).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now); + + StepVerifier.create(result) + .assertNext(assertNotModified(null, now)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void ifModifiedSinceShouldNotMatchIfDateAfter(String method) { + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifModifiedSince(oneMinuteAgo.toEpochMilli()).build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now); + + StepVerifier.create(result).verifyComplete(); + } + + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndDate(String method) { + String eTag = "\"Foo\""; + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch(eTag).ifModifiedSince(now.toEpochMilli()) + .build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now, eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(eTag, now)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndLaterDate(String method) { + String eTag = "\"Foo\""; + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch(eTag).ifModifiedSince(oneMinuteAgo.toEpochMilli()) + .build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now, eTag); + + StepVerifier.create(result) + .assertNext(assertNotModified(eTag, now)) + .verifyComplete(); + } + + @SafeHttpMethodsTest + void IfNoneMatchAndIfNotModifiedSinceShouldNotMatchWhenDifferentETag(String method) { + Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + MockServerHttpRequest mockRequest = MockServerHttpRequest + .method(HttpMethod.valueOf(method), "/") + .ifNoneMatch("\"Bar\"").ifModifiedSince(now.toEpochMilli()) + .build(); + DefaultServerRequest request = createRequest(mockRequest); + Mono result = request.checkNotModified(now, "\"Foo\""); + + StepVerifier.create(result).verifyComplete(); + } + + private Consumer assertPreconditionFailed() { + return serverResponse -> assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.PRECONDITION_FAILED); + } + + private Consumer assertNotModified(@Nullable String eTag, @Nullable Instant lastModified) { + return serverResponse -> { + assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); + if (eTag != null) { assertThat(serverResponse.headers().getETag()).isEqualTo(eTag); - }) - .verifyComplete(); - } + } + if (lastModified != null) { + assertThat(serverResponse.headers().getLastModified()).isEqualTo(lastModified.toEpochMilli()); + } + }; + } - @ParameterizedHttpMethodTest - void checkNotModifiedETagWithSeparatorChars(String method) { - String eTag = "\"Foo, Bar\""; - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(eTag); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(strings = {"GET", "HEAD"}) + @interface SafeHttpMethodsTest { - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(eTag); - - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); - assertThat(serverResponse.headers().getETag()).isEqualTo(eTag); - }) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkModifiedETag(String method) { - String currentETag = "\"Foo\""; - String oldEtag = "Bar"; - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(oldEtag); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(currentETag); - - StepVerifier.create(result) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedUnpaddedETag(String method) { - String eTag = "Foo"; - String paddedEtag = String.format("\"%s\"", eTag); - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(paddedEtag); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(eTag); - - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); - assertThat(serverResponse.headers().getETag()).isEqualTo(paddedEtag); - }) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkModifiedUnpaddedETag(String method) { - String currentETag = "Foo"; - String oldEtag = "Bar"; - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(oldEtag); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(currentETag); - - StepVerifier.create(result) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedWildcardIsIgnored(String method) { - String eTag = "\"Foo\""; - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch("*"); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(eTag); - - StepVerifier.create(result) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedETagAndTimestamp(String method) { - String eTag = "\"Foo\""; - Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(eTag); - headers.setIfModifiedSince(now); - - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(now, eTag); - - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); - assertThat(serverResponse.headers().getETag()).isEqualTo(eTag); - assertThat(serverResponse.headers().getLastModified()).isEqualTo(now.toEpochMilli()); - }) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkNotModifiedETagAndModifiedTimestamp(String method) { - String eTag = "\"Foo\""; - Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES); - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(eTag); - headers.setIfModifiedSince(oneMinuteAgo); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(now, eTag); - - StepVerifier.create(result) - .assertNext(serverResponse -> { - assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED); - assertThat(serverResponse.headers().getETag()).isEqualTo(eTag); - assertThat(serverResponse.headers().getLastModified()).isEqualTo(now.toEpochMilli()); - }) - .verifyComplete(); - } - - @ParameterizedHttpMethodTest - void checkModifiedETagAndNotModifiedTimestamp(String method) throws Exception { - String currentETag = "\"Foo\""; - String oldEtag = "\"Bar\""; - Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - HttpHeaders headers = new HttpHeaders(); - headers.setIfNoneMatch(oldEtag); - headers.setIfModifiedSince(now); - MockServerHttpRequest mockRequest = MockServerHttpRequest - .method(HttpMethod.valueOf(method), "/") - .headers(headers) - .build(); - - DefaultServerRequest request = - new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); - - Mono result = request.checkNotModified(now, currentETag); - - StepVerifier.create(result) - .verifyComplete(); - } - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - @ParameterizedTest(name = "[{index}] {0}") - @ValueSource(strings = {"GET", "HEAD"}) - @interface ParameterizedHttpMethodTest { + } }