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
This commit is contained in:
@@ -39,6 +39,7 @@ import org.springframework.data.r2dbc.dialect.DialectResolver;
|
||||
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
|
||||
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
|
||||
import org.springframework.data.relational.RelationalManagedTypes;
|
||||
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.NamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
import org.springframework.r2dbc.core.DatabaseClient;
|
||||
@@ -79,11 +80,10 @@ public class R2dbcDataAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings("deprecation")
|
||||
public R2dbcMappingContext r2dbcMappingContext(ObjectProvider<NamingStrategy> namingStrategy,
|
||||
R2dbcCustomConversions r2dbcCustomConversions, RelationalManagedTypes r2dbcManagedTypes) {
|
||||
R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext(
|
||||
namingStrategy.getIfAvailable(() -> NamingStrategy.INSTANCE));
|
||||
namingStrategy.getIfAvailable(() -> DefaultNamingStrategy.INSTANCE));
|
||||
relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder());
|
||||
relationalMappingContext.setManagedTypes(r2dbcManagedTypes);
|
||||
return relationalMappingContext;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive;
|
||||
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
/**
|
||||
* {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is
|
||||
* auto-configured for problem details support.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@ControllerAdvice
|
||||
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
|
||||
}
|
||||
@@ -74,6 +74,7 @@ import org.springframework.web.reactive.result.method.HandlerMethodArgumentResol
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
@@ -334,6 +335,18 @@ public class WebFluxAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(prefix = "spring.webflux.problemdetails", name = "enabled", havingValue = "true")
|
||||
static class ProblemDetailsErrorHandlingConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
|
||||
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
|
||||
return new ProblemDetailsExceptionHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore {
|
||||
|
||||
private final Duration timeout;
|
||||
|
||||
@@ -36,6 +36,8 @@ public class WebFluxProperties {
|
||||
|
||||
private final Format format = new Format();
|
||||
|
||||
private final Problemdetails problemdetails = new Problemdetails();
|
||||
|
||||
/**
|
||||
* Path pattern used for static resources.
|
||||
*/
|
||||
@@ -74,6 +76,10 @@ public class WebFluxProperties {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
public Problemdetails getProblemdetails() {
|
||||
return this.problemdetails;
|
||||
}
|
||||
|
||||
public String getStaticPathPattern() {
|
||||
return this.staticPathPattern;
|
||||
}
|
||||
@@ -133,4 +139,21 @@ public class WebFluxProperties {
|
||||
|
||||
}
|
||||
|
||||
public static class Problemdetails {
|
||||
|
||||
/**
|
||||
* Whether RFC 7807 Problem Details support should be enabled.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.servlet;
|
||||
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
/**
|
||||
* {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is
|
||||
* auto-configured for problem details support.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@ControllerAdvice
|
||||
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
|
||||
}
|
||||
@@ -114,6 +114,7 @@ import org.springframework.web.servlet.i18n.FixedLocaleResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.servlet.resource.EncodedResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||
@@ -643,6 +644,18 @@ public class WebMvcAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
|
||||
static class ProblemDetailsErrorHandlingConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
|
||||
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
|
||||
return new ProblemDetailsExceptionHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator to make
|
||||
* {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy}
|
||||
|
||||
@@ -106,6 +106,8 @@ public class WebMvcProperties {
|
||||
|
||||
private final Pathmatch pathmatch = new Pathmatch();
|
||||
|
||||
private final Problemdetails problemdetails = new Problemdetails();
|
||||
|
||||
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
|
||||
return this.messageCodesResolverFormat;
|
||||
}
|
||||
@@ -213,6 +215,10 @@ public class WebMvcProperties {
|
||||
return this.pathmatch;
|
||||
}
|
||||
|
||||
public Problemdetails getProblemdetails() {
|
||||
return this.problemdetails;
|
||||
}
|
||||
|
||||
public static class Async {
|
||||
|
||||
/**
|
||||
@@ -447,4 +453,21 @@ public class WebMvcProperties {
|
||||
|
||||
}
|
||||
|
||||
public static class Problemdetails {
|
||||
|
||||
/**
|
||||
* Whether RFC 7807 Problem Details support should be enabled.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
@@ -85,6 +86,7 @@ import org.springframework.web.reactive.resource.ResourceWebHandler;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -622,6 +624,25 @@ class WebFluxAutoConfigurationTests {
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(propertiesClass));
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsDisabledByDefault() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsEnabledAddsExceptionHandler() {
|
||||
this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsBacksOffWhenExceptionHandler() {
|
||||
this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true")
|
||||
.withUserConfiguration(CustomExceptionResolverConfiguration.class)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)
|
||||
.hasSingleBean(CustomExceptionResolver.class));
|
||||
}
|
||||
|
||||
private ContextConsumer<ReactiveWebApplicationContext> assertExchangeWithSession(
|
||||
Consumer<MockServerWebExchange> exchange) {
|
||||
return (context) -> {
|
||||
@@ -911,4 +932,19 @@ class WebFluxAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomExceptionResolverConfiguration {
|
||||
|
||||
@Bean
|
||||
CustomExceptionResolver customExceptionResolver() {
|
||||
return new CustomExceptionResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
static class CustomExceptionResolver extends ResponseEntityExceptionHandler {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
@@ -106,6 +107,7 @@ import org.springframework.web.servlet.i18n.FixedLocaleResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.resource.CachingResourceResolver;
|
||||
import org.springframework.web.servlet.resource.CachingResourceTransformer;
|
||||
@@ -959,6 +961,25 @@ class WebMvcAutoConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsDisabledByDefault() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsEnabledAddsExceptionHandler() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ProblemDetailsExceptionHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailsBacksOffWhenExceptionHandler() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true")
|
||||
.withUserConfiguration(CustomExceptionResolverConfiguration.class)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ProblemDetailsExceptionHandler.class)
|
||||
.hasSingleBean(CustomExceptionResolver.class));
|
||||
}
|
||||
|
||||
private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context,
|
||||
Consumer<ResourceHttpRequestHandler> handlerConsumer) {
|
||||
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
|
||||
@@ -1485,4 +1506,19 @@ class WebMvcAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomExceptionResolverConfiguration {
|
||||
|
||||
@Bean
|
||||
CustomExceptionResolver customExceptionResolver() {
|
||||
return new CustomExceptionResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
static class CustomExceptionResolver extends ResponseEntityExceptionHandler {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -123,6 +123,23 @@ For machine clients, it produces a JSON response with details of the error, the
|
||||
For browser clients, there is a "`whitelabel`" error handler that renders the same data in HTML format.
|
||||
You can also provide your own HTML templates to display errors (see the <<web#web.reactive.webflux.error-handling.error-pages,next section>>).
|
||||
|
||||
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.
|
||||
Spring WebFlux can produce custom error messages with the `application/problem+json` media type, like:
|
||||
|
||||
[source,json,indent=0,subs="verbatim"]
|
||||
----
|
||||
{
|
||||
"type": "https://example.org/problems/unknown-project",
|
||||
"title": "Unknown project",
|
||||
"status": 404,
|
||||
"detail": "No project found for id 'spring-unknown'",
|
||||
"instance": "/projects/spring-unknown"
|
||||
}
|
||||
----
|
||||
|
||||
This support can be enabled by setting configprop:spring.webflux.problemdetails.enabled[] to `true`.
|
||||
|
||||
|
||||
The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents.
|
||||
For that, you can add a bean of type `ErrorAttributes`.
|
||||
|
||||
|
||||
@@ -298,6 +298,22 @@ TIP: The `BasicErrorController` can be used as a base class for a custom `ErrorC
|
||||
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).
|
||||
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.
|
||||
|
||||
As of Spring Framework 6.0, {spring-framework-docs}/web.html#mvc-ann-rest-exceptions[RFC 7807 Problem Details] is supported.
|
||||
Spring MVC can produce custom error messages with the `application/problem+json` media type, like:
|
||||
|
||||
[source,json,indent=0,subs="verbatim"]
|
||||
----
|
||||
{
|
||||
"type": "https://example.org/problems/unknown-project",
|
||||
"title": "Unknown project",
|
||||
"status": 404,
|
||||
"detail": "No project found for id 'spring-unknown'",
|
||||
"instance": "/projects/spring-unknown"
|
||||
}
|
||||
----
|
||||
|
||||
This support can be enabled by setting configprop:spring.mvc.problemdetails.enabled[] to `true`.
|
||||
|
||||
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:
|
||||
|
||||
include::code:MyControllerAdvice[]
|
||||
|
||||
Reference in New Issue
Block a user