Skip to content

Commit 23a9818

Browse files
committed
Auto-configure ProblemDetails support
This commit auto-configures ProblemDetails support for both Spring MVC and Spring WebFlux, contributing a `@ControllerAdvice` annotated `ResponseEntityExceptionHandler` bean if the `spring.mvc.problemdetails.enabled` or `spring.webflux.problemdetails.enabled` properties are set to `true`. Closes gh-32634
1 parent 13e0a13 commit 23a9818

File tree

11 files changed

+241
-2
lines changed

11 files changed

+241
-2
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
4040
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
4141
import org.springframework.data.relational.RelationalManagedTypes;
42+
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
4243
import org.springframework.data.relational.core.mapping.NamingStrategy;
4344
import org.springframework.data.relational.core.mapping.Table;
4445
import org.springframework.r2dbc.core.DatabaseClient;
@@ -79,11 +80,10 @@ static RelationalManagedTypes r2dbcManagedTypes(ApplicationContext applicationCo
7980

8081
@Bean
8182
@ConditionalOnMissingBean
82-
@SuppressWarnings("deprecation")
8383
public R2dbcMappingContext r2dbcMappingContext(ObjectProvider<NamingStrategy> namingStrategy,
8484
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
8585
R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext(
86-
namingStrategy.getIfAvailable(() -> NamingStrategy.INSTANCE));
86+
namingStrategy.getIfAvailable(() -> DefaultNamingStrategy.INSTANCE));
8787
relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder());
8888
relationalMappingContext.setManagedTypes(r2dbcManagedTypes);
8989
return relationalMappingContext;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive;
18+
19+
import org.springframework.web.bind.annotation.ControllerAdvice;
20+
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
21+
22+
/**
23+
* {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is
24+
* auto-configured for problem details support.
25+
*
26+
* @author Brian Clozel
27+
*/
28+
@ControllerAdvice
29+
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
30+
31+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java

+13
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
7575
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
7676
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
77+
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
7778
import org.springframework.web.reactive.result.view.ViewResolver;
7879
import org.springframework.web.server.WebSession;
7980
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@@ -334,6 +335,18 @@ ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCu
334335

335336
}
336337

338+
@Configuration(proxyBeanMethods = false)
339+
@ConditionalOnProperty(prefix = "spring.webflux.problemdetails", name = "enabled", havingValue = "true")
340+
static class ProblemDetailsErrorHandlingConfiguration {
341+
342+
@Bean
343+
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
344+
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
345+
return new ProblemDetailsExceptionHandler();
346+
}
347+
348+
}
349+
337350
static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore {
338351

339352
private final Duration timeout;

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java

+23
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class WebFluxProperties {
3636

3737
private final Format format = new Format();
3838

39+
private final Problemdetails problemdetails = new Problemdetails();
40+
3941
/**
4042
* Path pattern used for static resources.
4143
*/
@@ -74,6 +76,10 @@ public Format getFormat() {
7476
return this.format;
7577
}
7678

79+
public Problemdetails getProblemdetails() {
80+
return this.problemdetails;
81+
}
82+
7783
public String getStaticPathPattern() {
7884
return this.staticPathPattern;
7985
}
@@ -133,4 +139,21 @@ public void setDateTime(String dateTime) {
133139

134140
}
135141

142+
public static class Problemdetails {
143+
144+
/**
145+
* Whether RFC 7807 Problem Details support should be enabled.
146+
*/
147+
private boolean enabled = false;
148+
149+
public boolean isEnabled() {
150+
return this.enabled;
151+
}
152+
153+
public void setEnabled(boolean enabled) {
154+
this.enabled = enabled;
155+
}
156+
157+
}
158+
136159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.servlet;
18+
19+
import org.springframework.web.bind.annotation.ControllerAdvice;
20+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
21+
22+
/**
23+
* {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is
24+
* auto-configured for problem details support.
25+
*
26+
* @author Brian Clozel
27+
*/
28+
@ControllerAdvice
29+
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
30+
31+
}

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

+13
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
115115
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
116116
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
117+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
117118
import org.springframework.web.servlet.resource.EncodedResourceResolver;
118119
import org.springframework.web.servlet.resource.ResourceResolver;
119120
import org.springframework.web.servlet.resource.ResourceUrlProvider;
@@ -643,6 +644,18 @@ private ResourceResolver getVersionResourceResolver(Strategy properties) {
643644

644645
}
645646

647+
@Configuration(proxyBeanMethods = false)
648+
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
649+
static class ProblemDetailsErrorHandlingConfiguration {
650+
651+
@Bean
652+
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
653+
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
654+
return new ProblemDetailsExceptionHandler();
655+
}
656+
657+
}
658+
646659
/**
647660
* Decorator to make
648661
* {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy}

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

+23
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ public class WebMvcProperties {
106106

107107
private final Pathmatch pathmatch = new Pathmatch();
108108

109+
private final Problemdetails problemdetails = new Problemdetails();
110+
109111
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
110112
return this.messageCodesResolverFormat;
111113
}
@@ -213,6 +215,10 @@ public Pathmatch getPathmatch() {
213215
return this.pathmatch;
214216
}
215217

218+
public Problemdetails getProblemdetails() {
219+
return this.problemdetails;
220+
}
221+
216222
public static class Async {
217223

218224
/**
@@ -447,4 +453,21 @@ public enum MatchingStrategy {
447453

448454
}
449455

456+
public static class Problemdetails {
457+
458+
/**
459+
* Whether RFC 7807 Problem Details support should be enabled.
460+
*/
461+
private boolean enabled = false;
462+
463+
public boolean isEnabled() {
464+
return this.enabled;
465+
}
466+
467+
public void setEnabled(boolean enabled) {
468+
this.enabled = enabled;
469+
}
470+
471+
}
472+
450473
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.springframework.util.StringUtils;
7171
import org.springframework.validation.Validator;
7272
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
73+
import org.springframework.web.bind.annotation.ControllerAdvice;
7374
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
7475
import org.springframework.web.reactive.HandlerMapping;
7576
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@@ -85,6 +86,7 @@
8586
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
8687
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
8788
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
89+
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
8890
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
8991
import org.springframework.web.reactive.result.view.ViewResolver;
9092
import org.springframework.web.server.ServerWebExchange;
@@ -622,6 +624,25 @@ void propertiesAreNotEnabledInNonWebApplication(Class<?> propertiesClass) {
622624
.run((context) -> assertThat(context).doesNotHaveBean(propertiesClass));
623625
}
624626

627+
@Test
628+
void problemDetailsDisabledByDefault() {
629+
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class));
630+
}
631+
632+
@Test
633+
void problemDetailsEnabledAddsExceptionHandler() {
634+
this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true")
635+
.run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class));
636+
}
637+
638+
@Test
639+
void problemDetailsBacksOffWhenExceptionHandler() {
640+
this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true")
641+
.withUserConfiguration(CustomExceptionResolverConfiguration.class)
642+
.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)
643+
.hasSingleBean(CustomExceptionResolver.class));
644+
}
645+
625646
private ContextConsumer<ReactiveWebApplicationContext> assertExchangeWithSession(
626647
Consumer<MockServerWebExchange> exchange) {
627648
return (context) -> {
@@ -911,4 +932,19 @@ static class LowPrecedenceConfigurer implements WebFluxConfigurer {
911932

912933
}
913934

935+
@Configuration(proxyBeanMethods = false)
936+
static class CustomExceptionResolverConfiguration {
937+
938+
@Bean
939+
CustomExceptionResolver customExceptionResolver() {
940+
return new CustomExceptionResolver();
941+
}
942+
943+
}
944+
945+
@ControllerAdvice
946+
static class CustomExceptionResolver extends ResponseEntityExceptionHandler {
947+
948+
}
949+
914950
}

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

+36
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
7878
import org.springframework.web.accept.ContentNegotiationManager;
7979
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
80+
import org.springframework.web.bind.annotation.ControllerAdvice;
8081
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
8182
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
8283
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -106,6 +107,7 @@
106107
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
107108
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
108109
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
110+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
109111
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
110112
import org.springframework.web.servlet.resource.CachingResourceResolver;
111113
import org.springframework.web.servlet.resource.CachingResourceTransformer;
@@ -959,6 +961,25 @@ void addResourceHandlersAppliesToChildAndParentContext() {
959961
}
960962
}
961963

964+
@Test
965+
void problemDetailsDisabledByDefault() {
966+
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class));
967+
}
968+
969+
@Test
970+
void problemDetailsEnabledAddsExceptionHandler() {
971+
this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true")
972+
.run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class));
973+
}
974+
975+
@Test
976+
void problemDetailsBacksOffWhenExceptionHandler() {
977+
this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true")
978+
.withUserConfiguration(CustomExceptionResolverConfiguration.class)
979+
.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)
980+
.hasSingleBean(CustomExceptionResolver.class));
981+
}
982+
962983
private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context,
963984
Consumer<ResourceHttpRequestHandler> handlerConsumer) {
964985
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
@@ -1485,4 +1506,19 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
14851506

14861507
}
14871508

1509+
@Configuration(proxyBeanMethods = false)
1510+
static class CustomExceptionResolverConfiguration {
1511+
1512+
@Bean
1513+
CustomExceptionResolver customExceptionResolver() {
1514+
return new CustomExceptionResolver();
1515+
}
1516+
1517+
}
1518+
1519+
@ControllerAdvice
1520+
static class CustomExceptionResolver extends ResponseEntityExceptionHandler {
1521+
1522+
}
1523+
14881524
}

spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc

+17
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,23 @@ For machine clients, it produces a JSON response with details of the error, the
123123
For browser clients, there is a "`whitelabel`" error handler that renders the same data in HTML format.
124124
You can also provide your own HTML templates to display errors (see the <<web#web.reactive.webflux.error-handling.error-pages,next section>>).
125125

126+
Before customizing error handling in Spring Boot directly, you can leverage the {spring-framework-docs}/web-reactive.html#webflux-ann-rest-exceptions[RFC 7807 Problem Details] support in Spring WebFlux.
127+
Spring WebFlux can produce custom error messages with the `application/problem+json` media type, like:
128+
129+
[source,json,indent=0,subs="verbatim"]
130+
----
131+
{
132+
"type": "https://example.org/problems/unknown-project",
133+
"title": "Unknown project",
134+
"status": 404,
135+
"detail": "No project found for id 'spring-unknown'",
136+
"instance": "/projects/spring-unknown"
137+
}
138+
----
139+
140+
This support can be enabled by setting configprop:spring.webflux.problemdetails.enabled[] to `true`.
141+
142+
126143
The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents.
127144
For that, you can add a bean of type `ErrorAttributes`.
128145

spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc

+16
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,22 @@ TIP: The `BasicErrorController` can be used as a base class for a custom `ErrorC
298298
This is particularly useful if you want to add a handler for a new content type (the default is to handle `text/html` specifically and provide a fallback for everything else).
299299
To do so, extend `BasicErrorController`, add a public method with a `@RequestMapping` that has a `produces` attribute, and create a bean of your new type.
300300

301+
As of Spring Framework 6.0, {spring-framework-docs}/web.html#mvc-ann-rest-exceptions[RFC 7807 Problem Details] is supported.
302+
Spring MVC can produce custom error messages with the `application/problem+json` media type, like:
303+
304+
[source,json,indent=0,subs="verbatim"]
305+
----
306+
{
307+
"type": "https://example.org/problems/unknown-project",
308+
"title": "Unknown project",
309+
"status": 404,
310+
"detail": "No project found for id 'spring-unknown'",
311+
"instance": "/projects/spring-unknown"
312+
}
313+
----
314+
315+
This support can be enabled by setting configprop:spring.mvc.problemdetails.enabled[] to `true`.
316+
301317
You can also define a class annotated with `@ControllerAdvice` to customize the JSON document to return for a particular controller and/or exception type, as shown in the following example:
302318

303319
include::code:MyControllerAdvice[]

0 commit comments

Comments
 (0)