Skip to content

Commit 67e5897

Browse files
committed
Disable suffix pattern matching in Spring MVC
This commit disables by default suffix pattern matching in Spring MVC applications. As described in the Spring MVC documentation (see https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestmapping-suffix-pattern-match), this is considered as best practice. This change also introduces new configuration properties to achieve similar results in a safer way (using query parameters) or to rollback to the former default. Closes gh-11105
1 parent 2bf662f commit 67e5897

File tree

5 files changed

+227
-10
lines changed

5 files changed

+227
-10
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -101,6 +101,7 @@
101101
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
102102
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
103103
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
104+
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
104105
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
105106
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
106107
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -214,8 +215,23 @@ public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
214215
}
215216
}
216217

218+
@Override
219+
public void configurePathMatch(PathMatchConfigurer configurer) {
220+
configurer.setUseSuffixPatternMatch(this.mvcProperties
221+
.getPathMatch().isUseSuffixPattern());
222+
configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties
223+
.getPathMatch().isUseRegisteredSuffixPattern());
224+
}
225+
217226
@Override
218227
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
228+
WebMvcProperties.ContentNegotiation contentNegotiation
229+
= this.mvcProperties.getContentNegotiation();
230+
configurer.favorPathExtension(contentNegotiation.isFavorPathExtension());
231+
configurer.favorParameter(contentNegotiation.isFavorParameter());
232+
if (contentNegotiation.getParameterName() != null) {
233+
configurer.parameterName(contentNegotiation.getParameterName());
234+
}
219235
Map<String, MediaType> mediaTypes = this.mvcProperties.getMediaTypes();
220236
for (Entry<String, MediaType> mediaType : mediaTypes.entrySet()) {
221237
configurer.mediaType(mediaType.getKey(), mediaType.getValue());
@@ -308,17 +324,17 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
308324
registry.addResourceHandler("/webjars/**")
309325
.addResourceLocations(
310326
"classpath:/META-INF/resources/webjars/")
311-
.setCachePeriod(getSeconds(cachePeriod))
312-
.setCacheControl(cacheControl));
327+
.setCachePeriod(getSeconds(cachePeriod))
328+
.setCacheControl(cacheControl));
313329
}
314330
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
315331
if (!registry.hasMappingForPattern(staticPathPattern)) {
316332
customizeResourceHandlerRegistration(
317333
registry.addResourceHandler(staticPathPattern)
318334
.addResourceLocations(getResourceLocations(
319335
this.resourceProperties.getStaticLocations()))
320-
.setCachePeriod(getSeconds(cachePeriod))
321-
.setCacheControl(cacheControl));
336+
.setCachePeriod(getSeconds(cachePeriod))
337+
.setCacheControl(cacheControl));
322338
}
323339
}
324340

@@ -450,8 +466,8 @@ public EnableWebMvcConfiguration(
450466
@Override
451467
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
452468
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
453-
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null ? true
454-
: this.mvcProperties.isIgnoreDefaultModelOnRedirect());
469+
adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null
470+
|| this.mvcProperties.isIgnoreDefaultModelOnRedirect());
455471
return adapter;
456472
}
457473

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java

+92-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
* @author Sébastien Deleuze
3333
* @author Stephane Nicoll
3434
* @author Eddú Meléndez
35+
* @author Brian Clozel
3536
* @since 1.1
3637
*/
3738
@ConfigurationProperties(prefix = "spring.mvc")
@@ -102,6 +103,10 @@ public class WebMvcProperties {
102103

103104
private final View view = new View();
104105

106+
private final ContentNegotiation contentNegotiation = new ContentNegotiation();
107+
108+
private final PathMatch pathMatch = new PathMatch();
109+
105110
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
106111
return this.messageCodesResolverFormat;
107112
}
@@ -204,6 +209,14 @@ public View getView() {
204209
return this.view;
205210
}
206211

212+
public ContentNegotiation getContentNegotiation() {
213+
return this.contentNegotiation;
214+
}
215+
216+
public PathMatch getPathMatch() {
217+
return this.pathMatch;
218+
}
219+
207220
public static class Async {
208221

209222
/**
@@ -270,6 +283,84 @@ public void setSuffix(String suffix) {
270283

271284
}
272285

286+
public static class ContentNegotiation {
287+
288+
/**
289+
* Whether the path extension in the URL path should be used to determine
290+
* the requested media type. If enabled a request "/users.pdf" will be
291+
* interpreted as a request for "application/pdf" regardless of the 'Accept' header.
292+
*/
293+
private boolean favorPathExtension = false;
294+
295+
/**
296+
* Whether a request parameter ("format" by default) should be used to
297+
* determine the requested media type.
298+
*/
299+
private boolean favorParameter = false;
300+
301+
/**
302+
* Query parameter name to use when "favor-parameter" is enabled.
303+
*/
304+
private String parameterName;
305+
306+
public boolean isFavorPathExtension() {
307+
return this.favorPathExtension;
308+
}
309+
310+
public void setFavorPathExtension(boolean favorPathExtension) {
311+
this.favorPathExtension = favorPathExtension;
312+
}
313+
314+
public boolean isFavorParameter() {
315+
return this.favorParameter;
316+
}
317+
318+
public void setFavorParameter(boolean favorParameter) {
319+
this.favorParameter = favorParameter;
320+
}
321+
322+
public String getParameterName() {
323+
return this.parameterName;
324+
}
325+
326+
public void setParameterName(String parameterName) {
327+
this.parameterName = parameterName;
328+
}
329+
}
330+
331+
public static class PathMatch {
332+
333+
/**
334+
* Whether to use suffix pattern match (".*") when matching patterns to
335+
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
336+
*/
337+
private boolean useSuffixPattern = false;
338+
339+
/**
340+
* Whether suffix pattern matching should work only against path extensions
341+
* explicitly registered with "spring.mvc.media-types.*".
342+
* This is generally recommended to reduce ambiguity and to
343+
* avoid issues such as when a "." appears in the path for other reasons.
344+
*/
345+
private boolean useRegisteredSuffixPattern = false;
346+
347+
public boolean isUseSuffixPattern() {
348+
return this.useSuffixPattern;
349+
}
350+
351+
public void setUseSuffixPattern(boolean useSuffixPattern) {
352+
this.useSuffixPattern = useSuffixPattern;
353+
}
354+
355+
public boolean isUseRegisteredSuffixPattern() {
356+
return this.useRegisteredSuffixPattern;
357+
}
358+
359+
public void setUseRegisteredSuffixPattern(boolean useRegisteredSuffixPattern) {
360+
this.useRegisteredSuffixPattern = useRegisteredSuffixPattern;
361+
}
362+
}
363+
273364
public enum LocaleResolver {
274365

275366
/**

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java

+61-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@
6262
import org.springframework.validation.Validator;
6363
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
6464
import org.springframework.web.accept.ContentNegotiationManager;
65+
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
6566
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
6667
import org.springframework.web.filter.HttpPutFormContentFilter;
6768
import org.springframework.web.servlet.HandlerAdapter;
@@ -470,7 +471,8 @@ public void customAsyncRequestTimeout() {
470471

471472
@Test
472473
public void customMediaTypes() {
473-
this.contextRunner.withPropertyValues("spring.mvc.mediaTypes.yaml:text/yaml")
474+
this.contextRunner.withPropertyValues("spring.mvc.mediaTypes.yaml:text/yaml",
475+
"spring.mvc.content-negotiation.favor-path-extension:true")
474476
.run((context) -> {
475477
RequestMappingHandlerAdapter adapter = context
476478
.getBean(RequestMappingHandlerAdapter.class);
@@ -738,6 +740,63 @@ public void cacheControl() {
738740
.run((context) -> assertCacheControl(context));
739741
}
740742

743+
@Test
744+
public void defaultPathMatching() {
745+
this.contextRunner.run((context) -> {
746+
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
747+
assertThat(handlerMapping.useSuffixPatternMatch()).isFalse();
748+
assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isFalse();
749+
});
750+
}
751+
752+
@Test
753+
public void useSuffixPatternMatch() {
754+
this.contextRunner
755+
.withPropertyValues("spring.mvc.path-match.use-suffix-pattern:true",
756+
"spring.mvc.path-match.use-registered-suffix-pattern:true")
757+
.run((context) -> {
758+
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
759+
assertThat(handlerMapping.useSuffixPatternMatch()).isTrue();
760+
assertThat(handlerMapping.useRegisteredSuffixPatternMatch()).isTrue();
761+
});
762+
}
763+
764+
@Test
765+
public void defaultContentNegotiation() {
766+
this.contextRunner.run((context) -> {
767+
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
768+
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
769+
assertThat(contentNegotiationManager.getStrategies())
770+
.doesNotHaveAnyElementsOfTypes(WebMvcAutoConfiguration
771+
.OptionalPathExtensionContentNegotiationStrategy.class);
772+
});
773+
}
774+
775+
@Test
776+
public void pathExtensionContentNegotiation() {
777+
this.contextRunner
778+
.withPropertyValues("spring.mvc.content-negotiation.favor-path-extension:true")
779+
.run((context) -> {
780+
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
781+
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
782+
assertThat(contentNegotiationManager.getStrategies())
783+
.hasAtLeastOneElementOfType(WebMvcAutoConfiguration
784+
.OptionalPathExtensionContentNegotiationStrategy.class);
785+
});
786+
}
787+
788+
@Test
789+
public void queryParameterContentNegotiation() {
790+
this.contextRunner
791+
.withPropertyValues("spring.mvc.content-negotiation.favor-parameter:true")
792+
.run((context) -> {
793+
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
794+
ContentNegotiationManager contentNegotiationManager = handlerMapping.getContentNegotiationManager();
795+
assertThat(contentNegotiationManager.getStrategies())
796+
.hasAtLeastOneElementOfType(ParameterContentNegotiationStrategy.class);
797+
});
798+
}
799+
741800
private void assertCacheControl(AssertableWebApplicationContext context) {
742801
Map<String, Object> handlerMap = getHandlerMap(
743802
context.getBean("resourceHandlerMapping", HandlerMapping.class));

spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc

+5
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,9 @@ content into your application. Rather, pick only the properties that you need.
393393
394394
# SPRING MVC ({sc-spring-boot-autoconfigure}/web/servlet/WebMvcProperties.{sc-ext}[WebMvcProperties])
395395
spring.mvc.async.request-timeout= # Amount of time before asynchronous request handling times out.
396+
spring.mvc.content-negotiation.favor-path-extension=false # Whether the path extension in the URL path should be used to determine the requested media type.
397+
spring.mvc.content-negotiation.favor-parameter=false # Whether a request parameter ("format" by default) should be used to determine the requested media type.
398+
spring.mvc.content-negotiation.parameter-name= # Query parameter name to use when "favor-parameter" is enabled.
396399
spring.mvc.date-format= # Date format to use. For instance, `dd/MM/yyyy`.
397400
spring.mvc.dispatch-trace-request=false # Whether to dispatch TRACE requests to the FrameworkServlet doService method.
398401
spring.mvc.dispatch-options-request=true # Whether to dispatch OPTIONS requests to the FrameworkServlet doService method.
@@ -404,6 +407,8 @@ content into your application. Rather, pick only the properties that you need.
404407
spring.mvc.log-resolved-exception=false # Whether to enable warn logging of exceptions resolved by a "HandlerExceptionResolver".
405408
spring.mvc.media-types.*= # Maps file extensions to media types for content negotiation.
406409
spring.mvc.message-codes-resolver-format= # Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.
410+
spring.mvc.path-match.use-registered-suffix-pattern=false # Whether suffix pattern matching should work only against path extensions explicitly registered with "spring.mvc.media-types.*".
411+
spring.mvc.path-match.use-suffix-pattern=false # Whether to use suffix pattern match (".*") when matching patterns to requests.
407412
spring.mvc.servlet.load-on-startup=-1 # Load on startup priority of the Spring Web Services servlet.
408413
spring.mvc.static-path-pattern=/** # Path pattern used for static resources.
409414
spring.mvc.throw-exception-if-no-handler-found=false # Whether a "NoHandlerFoundException" should be thrown if no Handler was found to process a request.

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

+46
Original file line numberDiff line numberDiff line change
@@ -2053,6 +2053,52 @@ root of the classpath (in that order). If such a file is present, it is automati
20532053
used as the favicon of the application.
20542054

20552055

2056+
[[boot-features-spring-mvc-pathmatch]]
2057+
==== Path Patching and Content Negotiation
2058+
Spring MVC can map incoming HTTP requests to handlers by looking at the request path and
2059+
matching it to the mappings defined in your application (for example, `@GetMapping`
2060+
annotations on Controller methods).
2061+
2062+
Spring Boot chooses to disable suffix pattern matching by default, which means that
2063+
requests like `"GET /projects/spring-boot.json"` won't be matched to
2064+
`@GetMapping("/project/spring-boot")` mappings.
2065+
This is considered as a
2066+
{spring-reference}web.html#mvc-ann-requestmapping-suffix-pattern-match[best practice
2067+
for Spring MVC applications]. This feature was mainly useful in the past for HTTP
2068+
clients which did not send proper "Accept" request headers; we needed to make sure
2069+
to send the correct Content Type to the client. Nowadays, Content Negotiation
2070+
is much more reliable.
2071+
2072+
There are other ways to deal with HTTP clients that don't consistently send proper
2073+
"Accept" request headers. Instead of using suffix matching, we can use a query
2074+
parameter to ensure that requests like `"GET /projects/spring-boot?format=json"`
2075+
will be mapped to `@GetMapping("/project/spring-boot")`:
2076+
2077+
[source,properties,indent=0,subs="verbatim,quotes,attributes"]
2078+
----
2079+
spring.mvc.content-negotiation.favor-parameter=true
2080+
2081+
# We can change the parameter name, which is "format" by default:
2082+
# spring.mvc.content-negotiation.parameter-name=myparam
2083+
2084+
# We can also register additional file extensions/media types with:
2085+
spring.mvc.media-types.markdown=text/markdown
2086+
----
2087+
2088+
If you understand the caveats and would still like your application to use
2089+
suffix pattern matching, the following configuration is required:
2090+
2091+
[source,properties,indent=0,subs="verbatim,quotes,attributes"]
2092+
----
2093+
spring.mvc.content-negotiation.favor-path-extension=true
2094+
2095+
# You can also restrict that feature to known extensions only
2096+
# spring.mvc.path-match.use-registered-suffix-pattern=true
2097+
2098+
# We can also register additional file extensions/media types with:
2099+
# spring.mvc.media-types.adoc=text/asciidoc
2100+
----
2101+
20562102

20572103
[[boot-features-spring-mvc-web-binding-initializer]]
20582104
==== ConfigurableWebBindingInitializer

0 commit comments

Comments
 (0)