Improve conditional requests support
Prior to this commit, Spring MVC and Spring WebFlux would not support conditional requests with `If-Match` preconditions. As underlined in the RFC9110 Section 13.1, those are related to the `If-None-Match` conditions, but this time only performing requests if the resource matches the given ETag. This feature, and in general the `"*"` request Etag, are generally useful to prevent "lost updates" when performing a POST/PUT request: we want to ensure that we're updating a version with a known version or create a new resource only if it doesn't exist already. This commit adds `If-Match` conditional requests support and ensures that both `If-Match` and `If-None-Match` work well with `"*"` request ETags. We can't rely on `checkNotModified(null)`, as the compiler can't decide between method variants accepting an ETag `String` or a Last Modified `long`. Instead, developers should use empty ETags `""` to signal that no resource is known on the server side. Closes gh-24881
This commit is contained in:
parent
a3d3667e64
commit
0783f0762d
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
@Override
|
||||||
public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {
|
public boolean checkNotModified(@Nullable String eTag, long lastModifiedTimestamp) {
|
||||||
HttpServletResponse response = getResponse();
|
HttpServletResponse response = getResponse();
|
||||||
if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
|
if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
|
||||||
return this.notModified;
|
return this.notModified;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate conditions in order of precedence.
|
// Evaluate conditions in order of precedence.
|
||||||
// See https://tools.ietf.org/html/rfc7232#section-6
|
// See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2
|
||||||
|
if (validateIfMatch(eTag)) {
|
||||||
if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
|
updateResponseStateChanging();
|
||||||
if (this.notModified && response != null) {
|
|
||||||
response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
|
|
||||||
}
|
|
||||||
return this.notModified;
|
return this.notModified;
|
||||||
}
|
}
|
||||||
|
// 2) If-Unmodified-Since
|
||||||
boolean validated = validateIfNoneMatch(etag);
|
else if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
|
||||||
if (!validated) {
|
updateResponseStateChanging();
|
||||||
|
return this.notModified;
|
||||||
|
}
|
||||||
|
// 3) If-None-Match
|
||||||
|
if (!validateIfNoneMatch(eTag)) {
|
||||||
|
// 4) If-Modified-Since
|
||||||
validateIfModifiedSince(lastModifiedTimestamp);
|
validateIfModifiedSince(lastModifiedTimestamp);
|
||||||
}
|
}
|
||||||
|
updateResponseIdempotent(eTag, lastModifiedTimestamp);
|
||||||
|
return this.notModified;
|
||||||
|
}
|
||||||
|
|
||||||
// Update response
|
private boolean validateIfMatch(@Nullable String eTag) {
|
||||||
if (response != null) {
|
Enumeration<String> ifMatchHeaders = getRequest().getHeaders(HttpHeaders.IF_MATCH);
|
||||||
boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
|
if (SAFE_METHODS.contains(getRequest().getMethod())) {
|
||||||
if (this.notModified) {
|
return false;
|
||||||
response.setStatus(isHttpGetOrHead ?
|
}
|
||||||
HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
|
if (!ifMatchHeaders.hasMoreElements()) {
|
||||||
}
|
return false;
|
||||||
if (isHttpGetOrHead) {
|
}
|
||||||
if (lastModifiedTimestamp > 0 && parseDateValue(response.getHeader(HttpHeaders.LAST_MODIFIED)) == -1) {
|
this.notModified = matchRequestedETags(ifMatchHeaders, eTag, false);
|
||||||
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateIfNoneMatch(@Nullable String eTag) {
|
||||||
|
Enumeration<String> ifNoneMatchHeaders = getRequest().getHeaders(HttpHeaders.IF_NONE_MATCH);
|
||||||
|
if (!ifNoneMatchHeaders.hasMoreElements()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.notModified = !matchRequestedETags(ifNoneMatchHeaders, eTag, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchRequestedETags(Enumeration<String> 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) {
|
if (weakCompare) {
|
||||||
response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag));
|
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) {
|
private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) {
|
||||||
|
|
@ -255,57 +323,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||||
if (ifUnmodifiedSince == -1) {
|
if (ifUnmodifiedSince == -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// We will perform this validation...
|
|
||||||
this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
|
this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateIfNoneMatch(@Nullable String etag) {
|
|
||||||
if (!StringUtils.hasLength(etag)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Enumeration<String> 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) {
|
private boolean validateIfModifiedSince(long lastModifiedTimestamp) {
|
||||||
if (lastModifiedTimestamp < 0) {
|
if (lastModifiedTimestamp < 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -319,6 +340,24 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||||
return true;
|
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() {
|
public boolean isNotModified() {
|
||||||
return this.notModified;
|
return this.notModified;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* Default implementation of {@link ServerWebExchange}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class DefaultServerWebExchange implements ServerWebExchange {
|
public class DefaultServerWebExchange implements ServerWebExchange {
|
||||||
|
|
@ -249,44 +250,135 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkNotModified(@Nullable String etag, Instant lastModified) {
|
public boolean checkNotModified(@Nullable String eTag, Instant lastModified) {
|
||||||
HttpStatusCode status = getResponse().getStatusCode();
|
HttpStatusCode status = getResponse().getStatusCode();
|
||||||
if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) {
|
if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) {
|
||||||
return this.notModified;
|
return this.notModified;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate conditions in order of precedence.
|
// Evaluate conditions in order of precedence.
|
||||||
// See https://tools.ietf.org/html/rfc7232#section-6
|
// See https://datatracker.ietf.org/doc/html/rfc9110#section-13.2.2
|
||||||
|
// 1) If-Match
|
||||||
if (validateIfUnmodifiedSince(lastModified)) {
|
if (validateIfMatch(eTag)) {
|
||||||
if (this.notModified) {
|
updateResponseStateChanging();
|
||||||
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
|
|
||||||
}
|
|
||||||
return this.notModified;
|
return this.notModified;
|
||||||
}
|
}
|
||||||
|
// 2) If-Unmodified-Since
|
||||||
boolean validated = validateIfNoneMatch(etag);
|
else if (validateIfUnmodifiedSince(lastModified)) {
|
||||||
if (!validated) {
|
updateResponseStateChanging();
|
||||||
|
return this.notModified;
|
||||||
|
}
|
||||||
|
// 3) If-None-Match
|
||||||
|
if (!validateIfNoneMatch(eTag)) {
|
||||||
|
// 4) If-Modified-Since
|
||||||
validateIfModifiedSince(lastModified);
|
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<String> 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) {
|
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);
|
HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED);
|
||||||
}
|
}
|
||||||
if (isHttpGetOrHead) {
|
if (isSafeMethod) {
|
||||||
if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) {
|
if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) {
|
||||||
getResponseHeaders().setLastModified(lastModified.toEpochMilli());
|
getResponseHeaders().setLastModified(lastModified.toEpochMilli());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) {
|
if (StringUtils.hasLength(eTag) && getResponseHeaders().getETag() == null) {
|
||||||
getResponseHeaders().setETag(padEtagIfNecessary(etag));
|
getResponseHeaders().setETag(padEtagIfNecessary(eTag));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notModified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateIfUnmodifiedSince(Instant lastModified) {
|
private boolean validateIfUnmodifiedSince(Instant lastModified) {
|
||||||
|
|
@ -297,56 +389,11 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
||||||
if (ifUnmodifiedSince == -1) {
|
if (ifUnmodifiedSince == -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// We will perform this validation...
|
|
||||||
Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince);
|
Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince);
|
||||||
this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS));
|
this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateIfNoneMatch(@Nullable String etag) {
|
|
||||||
if (!StringUtils.hasLength(etag)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
List<String> 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) {
|
private boolean validateIfModifiedSince(Instant lastModified) {
|
||||||
if (lastModified.isBefore(Instant.EPOCH)) {
|
if (lastModified.isBefore(Instant.EPOCH)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,17 @@ import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.ZonedDateTime;
|
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.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
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.MockHttpServletRequest;
|
||||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
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 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 MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||||
|
|
||||||
private final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
private final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||||
|
|
||||||
private final ServletWebRequest request = new ServletWebRequest(servletRequest, servletResponse);
|
private final ServletWebRequest request = new ServletWebRequest(servletRequest, servletResponse);
|
||||||
|
|
||||||
private final Date currentDate = new Date();
|
|
||||||
|
|
||||||
|
@Test
|
||||||
@ParameterizedHttpMethodTest
|
void ifMatchWildcardShouldMatchWhenETagPresent() {
|
||||||
void checkNotModifiedNon2xxStatus(String method) {
|
setUpRequest("PUT");
|
||||||
setUpRequest(method);
|
servletRequest.addHeader(HttpHeaders.IF_MATCH, "*");
|
||||||
|
assertThat(request.checkNotModified("\"SomeETag\"")).isFalse();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest // SPR-13516
|
@Test
|
||||||
void checkNotModifiedInvalidStatus(String method) {
|
void ifMatchWildcardShouldMatchETagMissing() {
|
||||||
setUpRequest(method);
|
setUpRequest("PUT");
|
||||||
|
servletRequest.addHeader(HttpHeaders.IF_MATCH, "*");
|
||||||
long epochTime = currentDate.getTime();
|
assertThat(request.checkNotModified("")).isTrue();
|
||||||
servletRequest.addHeader("If-Modified-Since", epochTime);
|
assertPreconditionFailed();
|
||||||
servletResponse.setStatus(0);
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(epochTime)).isFalse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest // SPR-14559
|
@Test
|
||||||
void checkNotModifiedInvalidIfNoneMatchHeader(String method) {
|
void ifMatchValueShouldMatchWhenETagMatches() {
|
||||||
setUpRequest(method);
|
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\"";
|
String etag = "\"etagvalue\"";
|
||||||
servletRequest.addHeader("If-None-Match", "missingquotes");
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "missingquotes");
|
||||||
assertThat(request.checkNotModified(etag)).isFalse();
|
assertThat(request.checkNotModified(etag)).isFalse();
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
assertOkWithETag(etag);
|
||||||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedHeaderAlreadySet(String method) {
|
void ifNoneMatchShouldMatchPaddedETag(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "spring";
|
||||||
long epochTime = currentDate.getTime();
|
String paddedEtag = String.format("\"%s\"", etag);
|
||||||
servletRequest.addHeader("If-Modified-Since", epochTime);
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, paddedEtag);
|
||||||
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);
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(etag)).isTrue();
|
assertThat(request.checkNotModified(etag)).isTrue();
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(304);
|
assertNotModified(paddedEtag, null);
|
||||||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedETagWithSeparatorChars(String method) {
|
void ifNoneMatchShouldIgnoreWildcard(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "\"spring\"";
|
||||||
String etag = "\"Foo, Bar\"";
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "*");
|
||||||
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", "*");
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(etag)).isFalse();
|
assertThat(request.checkNotModified(etag)).isFalse();
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
assertOkWithETag(etag);
|
||||||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@Test
|
||||||
void checkNotModifiedETagAndTimestamp(String method) {
|
void ifNoneMatchShouldRejectWildcardForUnsafeMethods() {
|
||||||
setUpRequest(method);
|
setUpRequest("PUT");
|
||||||
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "*");
|
||||||
String etag = "\"Foo\"";
|
assertThat(request.checkNotModified("\"spring\"")).isTrue();
|
||||||
servletRequest.addHeader("If-None-Match", etag);
|
assertPreconditionFailed();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest // SPR-14224
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedETagAndModifiedTimestamp(String method) {
|
void ifNoneMatchValueShouldUseWeakComparison(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "\"spring\"";
|
||||||
String etag = "\"Foo\"";
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "W/" + etag);
|
||||||
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));
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(etag)).isTrue();
|
assertThat(request.checkNotModified(etag)).isTrue();
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(304);
|
assertNotModified(etag, null);
|
||||||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedMultipleETags(String method) {
|
void ifModifiedSinceShouldMatchIfDatesEqual(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli());
|
||||||
String etag = "\"Bar\"";
|
assertThat(request.checkNotModified(NOW.toEpochMilli())).isTrue();
|
||||||
String multipleETags = String.format("\"Foo\", %s", etag);
|
assertNotModified(null, NOW);
|
||||||
servletRequest.addHeader("If-None-Match", multipleETags);
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(etag)).isTrue();
|
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(304);
|
|
||||||
assertThat(servletResponse.getHeader("ETag")).isEqualTo(etag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedTimestampWithLengthPart(String method) {
|
void ifModifiedSinceShouldNotMatchIfDateAfter(String method) {
|
||||||
setUpRequest(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();
|
long epochTime = ZonedDateTime.parse(CURRENT_TIME, RFC_1123_DATE_TIME).toInstant().toEpochMilli();
|
||||||
servletRequest.setMethod("GET");
|
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, "Wed, 09 Apr 2014 09:57:42 GMT; length=13774");
|
||||||
servletRequest.addHeader("If-Modified-Since", "Wed, 09 Apr 2014 09:57:42 GMT; length=13774");
|
|
||||||
|
|
||||||
assertThat(request.checkNotModified(epochTime)).isTrue();
|
assertThat(request.checkNotModified(epochTime)).isTrue();
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(304);
|
assertNotModified(null, Instant.ofEpochMilli(epochTime));
|
||||||
assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkModifiedTimestampWithLengthPart(String method) {
|
void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndDate(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "\"spring\"";
|
||||||
long epochTime = ZonedDateTime.parse(CURRENT_TIME, RFC_1123_DATE_TIME).toInstant().toEpochMilli();
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag);
|
||||||
servletRequest.setMethod("GET");
|
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, NOW.toEpochMilli());
|
||||||
servletRequest.addHeader("If-Modified-Since", "Wed, 08 Apr 2014 09:57:42 GMT; length=13774");
|
assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isTrue();
|
||||||
|
assertNotModified(etag, NOW);
|
||||||
assertThat(request.checkNotModified(epochTime)).isFalse();
|
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(servletResponse.getDateHeader("Last-Modified") / 1000).isEqualTo(epochTime / 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedTimestampConditionalPut(String method) {
|
void IfNoneMatchAndIfNotModifiedSinceShouldMatchWhenSameETagAndLaterDate(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "\"spring\"";
|
||||||
long currentEpoch = currentDate.getTime();
|
Instant oneMinuteLater = NOW.plus(1, ChronoUnit.MINUTES);
|
||||||
long oneMinuteAgo = currentEpoch - (1000 * 60);
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etag);
|
||||||
servletRequest.setMethod("PUT");
|
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, oneMinuteLater.toEpochMilli());
|
||||||
servletRequest.addHeader("If-UnModified-Since", currentEpoch);
|
assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isTrue();
|
||||||
|
assertNotModified(etag, NOW);
|
||||||
assertThat(request.checkNotModified(oneMinuteAgo)).isFalse();
|
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(servletResponse.getHeader("Last-Modified")).isNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@SafeHttpMethodsTest
|
||||||
void checkNotModifiedTimestampConditionalPutConflict(String method) {
|
void IfNoneMatchAndIfNotModifiedSinceShouldNotMatchWhenDifferentETag(String method) {
|
||||||
setUpRequest(method);
|
setUpRequest(method);
|
||||||
|
String etag = "\"framework\"";
|
||||||
long currentEpoch = currentDate.getTime();
|
Instant oneMinuteLater = NOW.plus(1, ChronoUnit.MINUTES);
|
||||||
long oneMinuteAgo = currentEpoch - (1000 * 60);
|
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, "\"spring\"");
|
||||||
servletRequest.setMethod("PUT");
|
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, oneMinuteLater.toEpochMilli());
|
||||||
servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo);
|
assertThat(request.checkNotModified(etag, NOW.toEpochMilli())).isFalse();
|
||||||
|
assertOkWithETag(etag);
|
||||||
assertThat(request.checkNotModified(currentEpoch)).isTrue();
|
assertOkWithLastModified(NOW);
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(412);
|
|
||||||
assertThat(servletResponse.getHeader("Last-Modified")).isNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setUpRequest(String method) {
|
private void setUpRequest(String method) {
|
||||||
this.servletRequest.setMethod(method);
|
this.servletRequest.setMethod(method);
|
||||||
this.servletRequest.setRequestURI("https://example.org");
|
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)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
@ParameterizedTest(name = "[{index}] {0}")
|
@ParameterizedTest(name = "[{index}] {0}")
|
||||||
@ValueSource(strings = { "GET", "HEAD" })
|
@ValueSource(strings = {"GET", "HEAD"})
|
||||||
@interface ParameterizedHttpMethodTest {
|
@interface SafeHttpMethodsTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Tests for {@link ShallowEtagHeaderFilter}.
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest {
|
||||||
*/
|
*/
|
||||||
B ifUnmodifiedSince(long ifUnmodifiedSince);
|
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.
|
* Set the values of the {@code If-None-Match} header.
|
||||||
* @param ifNoneMatches the new value of the header
|
* @param ifNoneMatches the new value of the header
|
||||||
|
|
@ -556,6 +562,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BodyBuilder ifMatch(String... ifMatches) {
|
||||||
|
this.headers.setIfMatch(Arrays.asList(ifMatches));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BodyBuilder ifNoneMatch(String... ifNoneMatches) {
|
public BodyBuilder ifNoneMatch(String... ifNoneMatches) {
|
||||||
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
|
this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.
|
* also with conditional POST/PUT/DELETE requests.
|
||||||
* <p><strong>Note:</strong> you can use either
|
* <p><strong>Note:</strong> you can use either
|
||||||
* this {@link #checkNotModified(Instant)} method; or
|
* 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,
|
* a strong entity tag and a Last-Modified value,
|
||||||
* as recommended by the HTTP specification,
|
* as recommended by the HTTP specification,
|
||||||
* then you should use {@link #checkNotModified(Instant, String)}.
|
* then you should use {@link #checkNotModified(Instant, String)}.
|
||||||
* @param etag the entity tag that the application determined
|
* @param etag the entity tag that the application determined
|
||||||
* for the underlying resource. This parameter will be padded
|
* 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
|
* @return a corresponding response if the request qualifies as not
|
||||||
* modified, or an empty result otherwise
|
* modified, or an empty result otherwise
|
||||||
* @since 5.2.5
|
* @since 5.2.5
|
||||||
|
|
@ -391,7 +392,8 @@ public interface ServerRequest {
|
||||||
* application determined for the underlying resource
|
* application determined for the underlying resource
|
||||||
* @param etag the entity tag that the application determined
|
* @param etag the entity tag that the application determined
|
||||||
* for the underlying resource. This parameter will be padded
|
* 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
|
* @return a corresponding response if the request qualifies as not
|
||||||
* modified, or an empty result otherwise.
|
* modified, or an empty result otherwise.
|
||||||
* @since 5.2.5
|
* @since 5.2.5
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalLong;
|
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.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
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.json.Jackson2JsonDecoder;
|
||||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||||
import org.springframework.http.codec.multipart.Part;
|
import org.springframework.http.codec.multipart.Part;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
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;
|
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Tests for {@link DefaultServerRequest} and {@link ServerRequest}.
|
||||||
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
* @author Brian Clozel
|
||||||
*/
|
*/
|
||||||
public class DefaultServerRequestTests {
|
public class DefaultServerRequestTests {
|
||||||
|
|
||||||
|
|
@ -237,139 +243,148 @@ public class DefaultServerRequestTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
public void body() {
|
class BodyTests {
|
||||||
byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8);
|
|
||||||
DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes));
|
|
||||||
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
|
||||||
|
|
||||||
HttpHeaders httpHeaders = new HttpHeaders();
|
@Test
|
||||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
public void body() {
|
||||||
|
byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8);
|
||||||
|
DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes));
|
||||||
|
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
||||||
|
|
||||||
MockServerHttpRequest mockRequest = MockServerHttpRequest
|
HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
.method(HttpMethod.GET, "https://example.com?foo=bar")
|
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||||
.headers(httpHeaders)
|
|
||||||
.body(body);
|
MockServerHttpRequest mockRequest = MockServerHttpRequest
|
||||||
DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders);
|
.method(HttpMethod.GET, "https://example.com?foo=bar")
|
||||||
|
.headers(httpHeaders)
|
||||||
|
.body(body);
|
||||||
|
DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), messageReaders);
|
||||||
|
|
||||||
|
Mono<String> 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<DataBuffer> 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<String> 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<DataBuffer> 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<String> typeReference = new ParameterizedTypeReference<>() {
|
||||||
|
};
|
||||||
|
Mono<String> 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<DataBuffer> 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<Map<String, String>> resultMono = request.bodyToMono(
|
||||||
|
new ParameterizedTypeReference<Map<String, String>>() {
|
||||||
|
});
|
||||||
|
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<DataBuffer> 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<String> 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<DataBuffer> 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<String> typeReference = new ParameterizedTypeReference<>() {
|
||||||
|
};
|
||||||
|
Flux<String> 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<DataBuffer> 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<String> resultFlux = request.bodyToFlux(String.class);
|
||||||
|
StepVerifier.create(resultFlux)
|
||||||
|
.expectError(UnsupportedMediaTypeStatusException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
Mono<String> 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<DataBuffer> 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<String> 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<DataBuffer> 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<String> typeReference = new ParameterizedTypeReference<>() {};
|
|
||||||
Mono<String> 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<DataBuffer> 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<Map<String, String>> resultMono = request.bodyToMono(
|
|
||||||
new ParameterizedTypeReference<Map<String, String>>() {});
|
|
||||||
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<DataBuffer> 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<String> 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<DataBuffer> 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<String> typeReference = new ParameterizedTypeReference<>() {};
|
|
||||||
Flux<String> 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<DataBuffer> 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<String> resultFlux = request.bodyToFlux(String.class);
|
|
||||||
StepVerifier.create(resultFlux)
|
|
||||||
.expectError(UnsupportedMediaTypeStatusException.class)
|
|
||||||
.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void formData() {
|
public void formData() {
|
||||||
|
|
@ -383,7 +398,7 @@ public class DefaultServerRequestTests {
|
||||||
.method(HttpMethod.GET, "https://example.com")
|
.method(HttpMethod.GET, "https://example.com")
|
||||||
.headers(httpHeaders)
|
.headers(httpHeaders)
|
||||||
.body(body);
|
.body(body);
|
||||||
DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
|
||||||
Mono<MultiValueMap<String, String>> resultData = request.formData();
|
Mono<MultiValueMap<String, String>> resultData = request.formData();
|
||||||
StepVerifier.create(resultData)
|
StepVerifier.create(resultData)
|
||||||
|
|
@ -416,7 +431,7 @@ public class DefaultServerRequestTests {
|
||||||
.method(HttpMethod.GET, "https://example.com")
|
.method(HttpMethod.GET, "https://example.com")
|
||||||
.headers(httpHeaders)
|
.headers(httpHeaders)
|
||||||
.body(body);
|
.body(body);
|
||||||
DefaultServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
|
||||||
Mono<MultiValueMap<String, Part>> resultData = request.multipartData();
|
Mono<MultiValueMap<String, Part>> resultData = request.multipartData();
|
||||||
StepVerifier.create(resultData)
|
StepVerifier.create(resultData)
|
||||||
|
|
@ -438,259 +453,293 @@ public class DefaultServerRequestTests {
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
private DefaultServerRequest createRequest(MockServerHttpRequest mockRequest) {
|
||||||
void checkNotModifiedTimestamp(String method) throws Exception {
|
return new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
||||||
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<ServerResponse> result = request.checkNotModified(now);
|
|
||||||
|
|
||||||
StepVerifier.create(result)
|
|
||||||
.assertNext(serverResponse -> {
|
|
||||||
assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
|
|
||||||
assertThat(serverResponse.headers().getLastModified()).isEqualTo(now.toEpochMilli());
|
|
||||||
})
|
|
||||||
.verifyComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 =
|
@Nested
|
||||||
new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
class CheckNotModifiedTests {
|
||||||
|
|
||||||
Mono<ServerResponse> result = request.checkNotModified(now);
|
@Test
|
||||||
|
void ifMatchWildcardShouldMatchWhenETagPresent() {
|
||||||
|
MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/")
|
||||||
|
.header(HttpHeaders.IF_MATCH, "*").build();
|
||||||
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
Mono<ServerResponse> result = request.checkNotModified("\"SomeETag\"");
|
||||||
|
|
||||||
StepVerifier.create(result)
|
StepVerifier.create(result)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@Test
|
||||||
void checkNotModifiedETag(String method) {
|
void ifMatchWildcardShouldMatchWhenETagMissing() {
|
||||||
String eTag = "\"Foo\"";
|
MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/")
|
||||||
HttpHeaders headers = new HttpHeaders();
|
.header(HttpHeaders.IF_MATCH, "*").build();
|
||||||
headers.setIfNoneMatch(eTag);
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
MockServerHttpRequest mockRequest = MockServerHttpRequest
|
Mono<ServerResponse> result = request.checkNotModified("");
|
||||||
.method(HttpMethod.valueOf(method), "/")
|
|
||||||
.headers(headers)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
DefaultServerRequest request =
|
StepVerifier.create(result)
|
||||||
new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
.assertNext(assertPreconditionFailed())
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
Mono<ServerResponse> result = request.checkNotModified(eTag);
|
@Test
|
||||||
|
void ifMatchValueShouldMatchWhenETagMatches() {
|
||||||
|
MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/")
|
||||||
|
.ifMatch("\"first\"", "\"second\"").build();
|
||||||
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
Mono<ServerResponse> result = request.checkNotModified("\"second\"");
|
||||||
|
|
||||||
StepVerifier.create(result)
|
StepVerifier.create(result).verifyComplete();
|
||||||
.assertNext(serverResponse -> {
|
}
|
||||||
assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
|
|
||||||
|
@Test
|
||||||
|
void ifMatchValueShouldNotMatchWhenETagDoesNotMatch() {
|
||||||
|
MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/")
|
||||||
|
.header(HttpHeaders.IF_MATCH, "\"first\"", "\"second\"").build();
|
||||||
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
Mono<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> result = request.checkNotModified("\"spring\"");
|
||||||
|
StepVerifier.create(result).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ifNoneMatchShouldRejectWildcardForUnsafeMethods() {
|
||||||
|
MockServerHttpRequest mockRequest = MockServerHttpRequest.put("/")
|
||||||
|
.ifNoneMatch("*").build();
|
||||||
|
DefaultServerRequest request = createRequest(mockRequest);
|
||||||
|
Mono<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> result = request.checkNotModified(now, "\"Foo\"");
|
||||||
|
|
||||||
|
StepVerifier.create(result).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<ServerResponse> assertPreconditionFailed() {
|
||||||
|
return serverResponse -> assertThat(serverResponse.statusCode()).isEqualTo(HttpStatus.PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<ServerResponse> 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);
|
assertThat(serverResponse.headers().getETag()).isEqualTo(eTag);
|
||||||
})
|
}
|
||||||
.verifyComplete();
|
if (lastModified != null) {
|
||||||
}
|
assertThat(serverResponse.headers().getLastModified()).isEqualTo(lastModified.toEpochMilli());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedHttpMethodTest
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
void checkNotModifiedETagWithSeparatorChars(String method) {
|
@Target(ElementType.METHOD)
|
||||||
String eTag = "\"Foo, Bar\"";
|
@ParameterizedTest(name = "[{index}] {0}")
|
||||||
HttpHeaders headers = new HttpHeaders();
|
@ValueSource(strings = {"GET", "HEAD"})
|
||||||
headers.setIfNoneMatch(eTag);
|
@interface SafeHttpMethodsTest {
|
||||||
MockServerHttpRequest mockRequest = MockServerHttpRequest
|
|
||||||
.method(HttpMethod.valueOf(method), "/")
|
|
||||||
.headers(headers)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
DefaultServerRequest request =
|
}
|
||||||
new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
|
|
||||||
|
|
||||||
Mono<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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 {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue