From a0af552d0f59365bf30c44fa3e0ed899521a60fa Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 6 Oct 2020 16:23:51 +0200 Subject: [PATCH] Add option for ignoring last-modified for static resources Prior to this commit, the resource handler serving static resources for Spring MVC and Spring WebFlux would always look at the `Resource#lastModified` information, derive the `"Last-Modified"` HTTP response header and support HTTP conditional requests with that information. In some cases, builds or packaging tools choose to set this last modification date to a static date in the past. This allows tools to have reproducible builds or to leverage caching given the static resources content didn't change. This can lead to problems where this static date (e.g. "1980-01-01") is used literally in HTTP responses and will make the HTTP caching mechanism counter-productive: the content of the resources changed, but the application insists on saying it didn't change since the 80s... This commit adds a new configuration option to disable this support - there is no way to automatically discard those dates: there is no standard for that and many don't use he "EPOCH 0 date" as it can lead to compatibility issues with different OSes. Closes gh-25845 --- .../config/ResourceHandlerRegistration.java | 15 +++++++++++ .../reactive/resource/ResourceWebHandler.java | 25 ++++++++++++++++++- .../config/ResourceHandlerRegistryTests.java | 6 +++++ .../resource/ResourceWebHandlerTests.java | 14 +++++++++++ .../ResourceHandlerRegistration.java | 18 ++++++++++++- .../resource/ResourceHttpRequestHandler.java | 25 ++++++++++++++++++- .../ResourceHandlerRegistryTests.java | 9 ++++++- .../ResourceHttpRequestHandlerTests.java | 12 +++++++++ src/docs/asciidoc/web/webmvc.adoc | 8 +++--- 9 files changed, 124 insertions(+), 8 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java index a4982ac8579..5686f6c8b55 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java @@ -48,6 +48,8 @@ public class ResourceHandlerRegistration { @Nullable private ResourceChainRegistration resourceChainRegistration; + private boolean useLastModified = true; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -94,6 +96,18 @@ public class ResourceHandlerRegistration { return this; } + /** + * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. + *

This configuration is set to {@code true} by default. + * @param useLastModified whether the "last modified" resource information should be used. + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3 + */ + public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -153,6 +167,7 @@ public class ResourceHandlerRegistration { if (this.cacheControl != null) { handler.setCacheControl(this.cacheControl); } + handler.setUseLastModified(this.useLastModified); return handler; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 87d59e7cd7b..9914780626d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -114,6 +114,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { @Nullable private ResourceLoader resourceLoader; + private boolean useLastModified = true; + /** * Accepts a list of String-based location values to be resolved into @@ -237,6 +239,27 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { this.resourceLoader = resourceLoader; } + /** + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 + */ + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether we should look at the {@link Resource#lastModified()} + * when serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata of + * the static files should be ignored. + * @param useLastModified whether to use the resource last-modified information. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + } @Override public void afterPropertiesSet() throws Exception { @@ -339,7 +362,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { } // Header phase - if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) { + if (isUseLastModified() && exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) { logger.trace(exchange.getLogPrefix() + "Resource not modified"); return Mono.empty(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java index ac140566429..3401d619724 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -212,6 +212,12 @@ public class ResourceHandlerRegistryTests { assertThat(transformers.get(2)).isSameAs(cssLinkTransformer); } + @Test + void ignoreLastModified() { + this.registration.setUseLastModified(false); + assertThat(getHandler("/resources/**").isUseLastModified()).isFalse(); + } + private ResourceWebHandler getHandler(String pathPattern) { SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index fc8f2922584..31052acbe9a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -631,6 +631,20 @@ public class ResourceWebHandlerTests { assertThat(exchange.getResponse().getHeaders().getCacheControl()).isEqualTo("max-age=3600"); } + @Test + void ignoreLastModified() { + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("")); + setPathWithinHandlerMapping(exchange, "foo.css"); + this.handler.setUseLastModified(false); + this.handler.handle(exchange).block(TIMEOUT); + + HttpHeaders headers = exchange.getResponse().getHeaders(); + assertThat(headers.getContentType()).isEqualTo(MediaType.parseMediaType("text/css")); + assertThat(headers.getContentLength()).isEqualTo(17); + assertThat(headers.containsKey("Last-Modified")).isFalse(); + assertResponseBody(exchange, "h1 { color:red; }"); + } + private void setPathWithinHandlerMapping(ServerWebExchange exchange, String path) { exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index 7c716bf4f35..f5c31028b7b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.List; import org.springframework.cache.Cache; +import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,6 +51,8 @@ public class ResourceHandlerRegistration { @Nullable private ResourceChainRegistration resourceChainRegistration; + private boolean useLastModified = true; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -109,6 +112,18 @@ public class ResourceHandlerRegistration { return this; } + /** + * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. + *

This configuration is set to {@code true} by default. + * @param useLastModified whether the "last modified" resource information should be used. + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3 + */ + public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -172,6 +187,7 @@ public class ResourceHandlerRegistration { else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } + handler.setUseLastModified(this.useLastModified); return handler; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 186c79d7bb2..21c8683d450 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -140,6 +140,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @Nullable private StringValueResolver embeddedValueResolver; + private boolean useLastModified = true; + public ResourceHttpRequestHandler() { super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); @@ -346,6 +348,27 @@ public class ResourceHttpRequestHandler extends WebContentGenerator this.embeddedValueResolver = resolver; } + /** + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 + */ + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether we should look at the {@link Resource#lastModified()} + * when serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata of + * the static files should be ignored. + * @param useLastModified whether to use the resource last-modified information. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + } @Override public void afterPropertiesSet() throws Exception { @@ -498,7 +521,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator checkRequest(request); // Header phase - if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { + if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified"); return; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index 0f51a1e82e5..0551598c492 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -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. @@ -238,6 +238,13 @@ public class ResourceHandlerRegistryTests { assertThat(locationCharsets.values().iterator().next()).isEqualTo(StandardCharsets.ISO_8859_1); } + @Test + void lastModifiedDisabled() { + this.registration.setUseLastModified(false); + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + assertThat(handler.isUseLastModified()).isFalse(); + } + private ResourceHttpRequestHandler getHandler(String pathPattern) { SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); return (ResourceHttpRequestHandler) hm.getUrlMap().get(pathPattern); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index 03a5167db90..6a17d8aa418 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -667,6 +667,18 @@ public class ResourceHttpRequestHandlerTests { assertThat(this.response.getHeader("Cache-Control")).isEqualTo("max-age=3600"); } + @Test + public void ignoreLastModified() throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.handler.setUseLastModified(false); + this.handler.handleRequest(this.request, this.response); + + assertThat(this.response.getContentType()).isEqualTo("text/css"); + assertThat(this.response.getContentLength()).isEqualTo(17); + assertThat(this.response.containsHeader("Last-Modified")).isFalse(); + assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); + } + private long resourceLastModified(String resourceName) throws IOException { return new ClassPathResource(resourceName, getClass()).getFile().lastModified(); diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 250626cb82e..b91245cd1f9 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -5621,8 +5621,8 @@ In the next example, given a request that starts with `/resources`, the relative used to find and serve static resources relative to `/public` under the web application root or on the classpath under `/static`. The resources are served with a one-year future expiration to ensure maximum use of the browser cache and a reduction in HTTP requests -made by the browser. The `Last-Modified` header is also evaluated and, if present, a `304` -status code is returned. +made by the browser. The `Last-Modified` information is deduced from `Resource#lastModified` +so that HTTP conditional requests are supported with `"Last-Modified"` headers. The following listing shows how to do so with Java configuration: @@ -5637,7 +5637,7 @@ The following listing shows how to do so with Java configuration: public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") .addResourceLocations("/public", "classpath:/static/") - .setCachePeriod(31556926); + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); } } ---- @@ -5651,7 +5651,7 @@ The following listing shows how to do so with Java configuration: override fun addResourceHandlers(registry: ResourceHandlerRegistry) { registry.addResourceHandler("/resources/**") .addResourceLocations("/public", "classpath:/static/") - .setCachePeriod(31556926) + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) } } ----