Add ServerRequest::checkNotModified

This commit adds the checkNotModified method to ServerRequest in both
WebFlux.fn and WebMvc.fn. Unlike other checkNotModified methods found
in the framework, this method does not return a boolean, but rather
a response wrapped in a Mono/Optional. If the resource has
not been changed, the not-modified response can be returned directly;
if the resource has changed, the user can create a corresponding
response using switchIfEmpty/orElse(Get).

Closes gh-24173
This commit is contained in:
Arjen Poutsma
2020-03-11 12:05:07 +01:00
parent 73e39726cd
commit 0dc1c7eb8b
7 changed files with 940 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -44,6 +45,7 @@ import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyExtractors;
@@ -86,6 +88,23 @@ class DefaultServerRequest implements ServerRequest {
this.headers = new DefaultHeaders();
}
static Mono<ServerResponse> checkNotModified(ServerWebExchange exchange, @Nullable Instant lastModified,
@Nullable String etag) {
if (lastModified == null) {
lastModified = Instant.MIN;
}
if (exchange.checkNotModified(etag, lastModified)) {
Integer statusCode = exchange.getResponse().getRawStatusCode();
return ServerResponse.status(statusCode != null ? statusCode : 200)
.headers(headers -> headers.addAll(exchange.getResponse().getHeaders()))
.build();
}
else {
return Mono.empty();
}
}
@Override
public String methodName() {

View File

@@ -20,6 +20,7 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.Principal;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -43,6 +44,7 @@ import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
@@ -287,6 +289,108 @@ public interface ServerRequest {
*/
ServerWebExchange exchange();
/**
* Check whether the requested resource has been modified given the
* supplied last-modified timestamp (as determined by the application).
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public Mono&lt;ServerResponse&gt; myHandleMethod(ServerRequest request) {
* Instant lastModified = // application-specific calculation
* return request.checkNotModified(lastModified)
* .switchIfEmpty(Mono.defer(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* }));
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> you can use either
* this {@code #checkNotModified(Instant)} method; or
* {@link #checkNotModified(String)}. If you want enforce both
* a strong entity tag and a Last-Modified value,
* as recommended by the HTTP specification,
* then you should use {@link #checkNotModified(Instant, String)}.
* @param lastModified the last-modified timestamp that the
* application determined for the underlying resource
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Mono<ServerResponse> checkNotModified(Instant lastModified) {
Assert.notNull(lastModified, "LastModified must not be null");
return DefaultServerRequest.checkNotModified(exchange(), lastModified, null);
}
/**
* Check whether the requested resource has been modified given the
* supplied {@code ETag} (entity tag), as determined by the application.
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public Mono&lt;ServerResponse&gt; myHandleMethod(ServerRequest request) {
* String eTag = // application-specific calculation
* return request.checkNotModified(eTag)
* .switchIfEmpty(Mono.defer(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* }));
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> you can use either
* this {@link #checkNotModified(Instant)} method; or
* {@code #checkNotModified(String)}. If you want enforce both
* a strong entity tag and a Last-Modified value,
* as recommended by the HTTP specification,
* then you should use {@link #checkNotModified(Instant, String)}.
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Mono<ServerResponse> checkNotModified(String etag) {
Assert.notNull(etag, "Etag must not be null");
return DefaultServerRequest.checkNotModified(exchange(), null, 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.
* If not modified, this method returns a response with corresponding
* status code and headers, otherwise an empty result.
* <p>Typical usage:
* <pre class="code">
* public Mono&lt;ServerResponse&gt; myHandleMethod(ServerRequest request) {
* Instant lastModified = // application-specific calculation
* String eTag = // application-specific calculation
* return request.checkNotModified(lastModified, eTag)
* .switchIfEmpty(Mono.defer(() -> {
* // further request processing, actually building content
* return ServerResponse.ok().body(...);
* }));
* }</pre>
* <p>This method works with conditional GET/HEAD requests, but
* also with conditional POST/PUT/DELETE requests.
* @param lastModified the last-modified timestamp that the
* application determined for the underlying resource
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
* @return a corresponding response if the request qualifies as not
* modified, or an empty result otherwise.
* @since 5.2.5
*/
default Mono<ServerResponse> checkNotModified(Instant lastModified, String etag) {
Assert.notNull(lastModified, "LastModified must not be null");
Assert.notNull(etag, "Etag must not be null");
return DefaultServerRequest.checkNotModified(exchange(), lastModified, etag);
}
// Static builder methods

View File

@@ -16,12 +16,18 @@
package org.springframework.web.reactive.function.server;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -30,6 +36,8 @@ import java.util.Optional;
import java.util.OptionalLong;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -43,6 +51,7 @@ import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
@@ -438,4 +447,260 @@ public class DefaultServerRequestTests {
.verifyComplete();
}
@ParameterizedHttpMethodTest
void checkNotModifiedTimestamp(String method) throws Exception {
Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
HttpHeaders headers = new HttpHeaders();
headers.setIfModifiedSince(now);
MockServerHttpRequest mockRequest = MockServerHttpRequest
.method(HttpMethod.valueOf(method), "/")
.headers(headers)
.build();
DefaultServerRequest request =
new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
Mono<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 =
new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
Mono<ServerResponse> result = request.checkNotModified(now);
StepVerifier.create(result)
.verifyComplete();
}
@ParameterizedHttpMethodTest
void checkNotModifiedETag(String method) {
String eTag = "\"Foo\"";
HttpHeaders headers = new HttpHeaders();
headers.setIfNoneMatch(eTag);
MockServerHttpRequest mockRequest = MockServerHttpRequest
.method(HttpMethod.valueOf(method), "/")
.headers(headers)
.build();
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 checkNotModifiedETagWithSeparatorChars(String method) {
String eTag = "\"Foo, Bar\"";
HttpHeaders headers = new HttpHeaders();
headers.setIfNoneMatch(eTag);
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 {
}
}