Skip to content

Commit 9ac5eb0

Browse files
committed
Support ETag generation on ResourceWebHandler
This commit replicates the ETag generation option now available on `ResourceHttpRequestHandler` but for its WebFlux counterpart. See gh-29031
1 parent ff14c51 commit 9ac5eb0

File tree

2 files changed

+72
-2
lines changed

2 files changed

+72
-2
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Locale;
2828
import java.util.Map;
2929
import java.util.Set;
30+
import java.util.function.Function;
3031

3132
import org.apache.commons.logging.Log;
3233
import org.apache.commons.logging.LogFactory;
@@ -123,6 +124,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
123124

124125
private boolean useLastModified = true;
125126

127+
@Nullable
128+
private Function<Resource, String> etagGenerator;
129+
126130
private boolean optimizeLocations = false;
127131

128132

@@ -275,6 +279,29 @@ public boolean isUseLastModified() {
275279
return this.useLastModified;
276280
}
277281

282+
/**
283+
* Configure a generator function that will be used to create the ETag information,
284+
* given a {@link Resource} that is about to be written to the response.
285+
* <p>This function should return a String that will be used as an argument in
286+
* {@link ServerWebExchange#checkNotModified(String)}, or {@code null} if no value
287+
* can be generated for the given resource.
288+
* @param etagGenerator the HTTP ETag generator function to use.
289+
* @since 6.1
290+
*/
291+
public void setEtagGenerator(@Nullable Function<Resource, String> etagGenerator) {
292+
this.etagGenerator = etagGenerator;
293+
}
294+
295+
/**
296+
* Return the HTTP ETag generator function to be used when serving resources.
297+
* @return the HTTP ETag generator function
298+
* @since 6.1
299+
*/
300+
@Nullable
301+
public Function<Resource, String> getEtagGenerator() {
302+
return this.etagGenerator;
303+
}
304+
278305
/**
279306
* Set whether to optimize the specified locations through an existence
280307
* check on startup, filtering non-existing directories upfront so that
@@ -418,7 +445,9 @@ public Mono<Void> handle(ServerWebExchange exchange) {
418445
}
419446

420447
// Header phase
421-
if (isUseLastModified() && exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) {
448+
String eTagValue = (this.getEtagGenerator() != null) ? this.getEtagGenerator().apply(resource) : null;
449+
Instant lastModified = isUseLastModified() ? Instant.ofEpochMilli(resource.lastModified()) : Instant.MIN;
450+
if (exchange.checkNotModified(eTagValue, lastModified)) {
422451
logger.trace(exchange.getLogPrefix() + "Resource not modified");
423452
return Mono.empty();
424453
}

spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,48 @@ void shouldRespondWithModifiedResource() throws Exception {
500500
}
501501

502502
@Test
503-
// SPR-14005
503+
void shouldRespondWithNotModifiedWhenEtag() throws Exception {
504+
this.handler.setEtagGenerator(resource -> "testEtag");
505+
this.handler.afterPropertiesSet();
506+
MockServerWebExchange exchange = MockServerWebExchange.from(
507+
MockServerHttpRequest.get("").ifNoneMatch( "\"testEtag\""));
508+
509+
setPathWithinHandlerMapping(exchange, "foo.css");
510+
setBestMachingPattern(exchange, "/**");
511+
this.handler.handle(exchange).block(TIMEOUT);
512+
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
513+
}
514+
515+
@Test
516+
void shouldRespondWithModifiedResourceWhenEtagNoMatch() throws Exception {
517+
this.handler.setEtagGenerator(resource -> "noMatch");
518+
this.handler.afterPropertiesSet();
519+
MockServerWebExchange exchange = MockServerWebExchange.from(
520+
MockServerHttpRequest.get("").ifNoneMatch( "\"testEtag\""));
521+
522+
setPathWithinHandlerMapping(exchange, "foo.css");
523+
setBestMachingPattern(exchange, "/**");
524+
this.handler.handle(exchange).block(TIMEOUT);
525+
assertThat((Object) exchange.getResponse().getStatusCode()).isNull();
526+
assertResponseBody(exchange, "h1 { color:red; }");
527+
}
528+
529+
@Test
530+
void shouldRespondWithNotModifiedWhenEtagAndLastModified() throws Exception {
531+
this.handler.setEtagGenerator(resource -> "testEtag");
532+
this.handler.afterPropertiesSet();
533+
MockServerWebExchange exchange = MockServerWebExchange.from(
534+
MockServerHttpRequest.get("")
535+
.ifModifiedSince(resourceLastModified("test/foo.css"))
536+
.ifNoneMatch( "\"testEtag\""));
537+
538+
setPathWithinHandlerMapping(exchange, "foo.css");
539+
setBestMachingPattern(exchange, "/**");
540+
this.handler.handle(exchange).block(TIMEOUT);
541+
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
542+
}
543+
544+
@Test // SPR-14005
504545
void doOverwriteExistingCacheControlHeaders() throws Exception {
505546
this.handler.setCacheControl(CacheControl.maxAge(3600, TimeUnit.SECONDS));
506547
this.handler.afterPropertiesSet();

0 commit comments

Comments
 (0)