Add checkNotModified support in ServerWebExchange
Issue: SPR-14522
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.web.server;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -67,4 +68,42 @@ public interface ServerWebExchange {
|
||||
*/
|
||||
Mono<WebSession> getSession();
|
||||
|
||||
/**
|
||||
* An overloaded variant of {@link #checkNotModified(String, Instant)} with
|
||||
* a last-modified timestamp only.
|
||||
* @param lastModified the last-modified time
|
||||
* @return whether the request qualifies as not modified
|
||||
*/
|
||||
boolean checkNotModified(Instant lastModified);
|
||||
|
||||
/**
|
||||
* An overloaded variant of {@link #checkNotModified(String, Instant)} with
|
||||
* an {@code ETag} (entity tag) value only.
|
||||
* @param etag the entity tag for the underlying resource.
|
||||
* @return true if the request does not require further processing.
|
||||
*/
|
||||
boolean checkNotModified(String etag);
|
||||
|
||||
/**
|
||||
* Check whether the requested resource has been modified given the supplied
|
||||
* {@code ETag} (entity tag) and last-modified timestamp as determined by
|
||||
* the application. Also transparently prepares the response, setting HTTP
|
||||
* status, and adding "ETag" and "Last-Modified" headers when applicable.
|
||||
* This method works with conditional GET/HEAD requests as well as with
|
||||
* conditional POST/PUT/DELETE requests.
|
||||
*
|
||||
* <p><strong>Note:</strong> The HTTP specification recommends setting both
|
||||
* ETag and Last-Modified values, but you can also use
|
||||
* {@code #checkNotModified(String)} or
|
||||
* {@link #checkNotModified(Instant)}.
|
||||
*
|
||||
* @param etag the entity tag that the application determined for the
|
||||
* underlying resource. This parameter will be padded with quotes (")
|
||||
* if necessary.
|
||||
* @param lastModified the last-modified timestamp that the application
|
||||
* determined for the underlying resource
|
||||
* @return true if the request does not require further processing.
|
||||
*/
|
||||
boolean checkNotModified(String etag, Instant lastModified);
|
||||
|
||||
}
|
||||
|
||||
@@ -16,15 +16,23 @@
|
||||
|
||||
package org.springframework.web.server.adapter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
@@ -37,6 +45,9 @@ import org.springframework.web.server.session.WebSessionManager;
|
||||
*/
|
||||
public class DefaultServerWebExchange implements ServerWebExchange {
|
||||
|
||||
private static final List<HttpMethod> SAFE_METHODS = Arrays.asList(HttpMethod.GET, HttpMethod.HEAD);
|
||||
|
||||
|
||||
private final ServerHttpRequest request;
|
||||
|
||||
private final ServerHttpResponse response;
|
||||
@@ -45,6 +56,8 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
||||
|
||||
private final Mono<WebSession> sessionMono;
|
||||
|
||||
private volatile boolean notModified;
|
||||
|
||||
|
||||
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSessionManager sessionManager) {
|
||||
@@ -63,11 +76,19 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
private HttpHeaders getRequestHeaders() {
|
||||
return getRequest().getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerHttpResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private HttpHeaders getResponseHeaders() {
|
||||
return getResponse().getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
@@ -83,4 +104,120 @@ public class DefaultServerWebExchange implements ServerWebExchange {
|
||||
return this.sessionMono;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkNotModified(Instant lastModified) {
|
||||
return checkNotModified(null, lastModified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkNotModified(String etag) {
|
||||
return checkNotModified(etag, Instant.MIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkNotModified(String etag, Instant lastModified) {
|
||||
HttpStatus status = getResponse().getStatusCode();
|
||||
if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) {
|
||||
return this.notModified;
|
||||
}
|
||||
|
||||
// Evaluate conditions in order of precedence.
|
||||
// See https://tools.ietf.org/html/rfc7232#section-6
|
||||
|
||||
if (validateIfUnmodifiedSince(lastModified)) {
|
||||
if (this.notModified) {
|
||||
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
return this.notModified;
|
||||
}
|
||||
|
||||
boolean validated = validateIfNoneMatch(etag);
|
||||
|
||||
if (!validated) {
|
||||
validateIfModifiedSince(lastModified);
|
||||
}
|
||||
|
||||
// Update response
|
||||
|
||||
boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
|
||||
if (this.notModified) {
|
||||
getResponse().setStatusCode(isHttpGetOrHead ?
|
||||
HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
if (isHttpGetOrHead) {
|
||||
if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) {
|
||||
getResponseHeaders().setLastModified(lastModified.toEpochMilli());
|
||||
}
|
||||
if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) {
|
||||
getResponseHeaders().setETag(padEtagIfNecessary(etag));
|
||||
}
|
||||
}
|
||||
|
||||
return this.notModified;
|
||||
}
|
||||
|
||||
private boolean validateIfUnmodifiedSince(Instant lastModified) {
|
||||
if (lastModified.isBefore(Instant.EPOCH)) {
|
||||
return false;
|
||||
}
|
||||
long ifUnmodifiedSince = getRequestHeaders().getIfUnmodifiedSince();
|
||||
if (ifUnmodifiedSince == -1) {
|
||||
return false;
|
||||
}
|
||||
// We will perform this validation...
|
||||
Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince);
|
||||
this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean validateIfNoneMatch(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);
|
||||
for (String clientETag : ifNoneMatch) {
|
||||
// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
|
||||
if (StringUtils.hasLength(clientETag) &&
|
||||
clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", ""))) {
|
||||
this.notModified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String padEtagIfNecessary(String etag) {
|
||||
if (!StringUtils.hasLength(etag)) {
|
||||
return etag;
|
||||
}
|
||||
if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
|
||||
return etag;
|
||||
}
|
||||
return "\"" + etag + "\"";
|
||||
}
|
||||
|
||||
private boolean validateIfModifiedSince(Instant lastModified) {
|
||||
if (lastModified.isBefore(Instant.EPOCH)) {
|
||||
return false;
|
||||
}
|
||||
long ifModifiedSince = getRequestHeaders().getIfModifiedSince();
|
||||
if (ifModifiedSince == -1) {
|
||||
return false;
|
||||
}
|
||||
// We will perform this validation...
|
||||
this.notModified = ChronoUnit.SECONDS.between(lastModified, Instant.ofEpochMilli(ifModifiedSince)) >= 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user