diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java index 86e66a2adac..86c2d647210 100644 --- a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java @@ -95,8 +95,19 @@ public interface ErrorResponse { } /** - * Resolve the {@link #getDetailMessageCode() detailMessageCode} through the - * given {@link MessageSource}, and if found, update the "detail" field. + * Return a code to use to resolve the problem "detail" for this exception + * through a {@link MessageSource}. + *

By default this is initialized via + * {@link #getDefaultDetailMessageCode(Class, String)}. + */ + default String getTitleCode() { + return getDefaultTitleMessageCode(getClass()); + } + + /** + * Resolve the {@link #getDetailMessageCode() detailMessageCode} and the + * {@link #getTitleCode() titleCode} through the given {@link MessageSource}, + * and if found, update the "detail" and "title!" fields respectively. * @param messageSource the {@code MessageSource} to use for the lookup * @param locale the {@code Locale} to use for the lookup */ @@ -107,22 +118,35 @@ public interface ErrorResponse { if (detail != null) { getBody().setDetail(detail); } + String title = messageSource.getMessage(getTitleCode(), null, null, locale); + if (title != null) { + getBody().setTitle(title); + } } return getBody(); } /** - * Build a message code for the given exception type, which consists of - * {@code "problemDetail."} followed by the full {@link Class#getName() class name}. - * @param exceptionType the exception type for which to build a code + * Build a message code for the "detail" field, for the given exception type. + * @param exceptionType the exception type associated with the problem * @param suffix an optional suffix, e.g. for exceptions that may have multiple * error message with different arguments. + * @return {@code "problemDetail."} followed by the full {@link Class#getName() class name} */ static String getDefaultDetailMessageCode(Class exceptionType, @Nullable String suffix) { return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + suffix : ""); } + /** + * Build a message code for the "title" field, for the given exception type. + * @param exceptionType the exception type associated with the problem + * @return {@code "problemDetail.title."} followed by the full {@link Class#getName() class name} + */ + static String getDefaultTitleMessageCode(Class exceptionType) { + return "problemDetail.title." + exceptionType.getName(); + } + /** * Map the given Exception to an {@link ErrorResponse}. * @param ex the Exception, mostly to derive message codes, if not provided diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java index d448b45b383..74c7ecc300d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -36,6 +36,7 @@ import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.ErrorResponse; import org.springframework.web.ErrorResponseException; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.server.MethodNotAllowedException; @@ -131,8 +132,11 @@ public class ResponseEntityExceptionHandlerTests { StaticMessageSource messageSource = new StaticMessageSource(); messageSource.addMessage( - "problemDetail." + UnsupportedMediaTypeStatusException.class.getName(), locale, + ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, null), locale, "Content-Type {0} not supported. Supported: {1}"); + messageSource.addMessage( + ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale, + "Media type is not valid or not supported"); this.exceptionHandler.setMessageSource(messageSource); @@ -147,6 +151,8 @@ public class ResponseEntityExceptionHandlerTests { ProblemDetail body = (ProblemDetail) responseEntity.getBody(); assertThat(body.getDetail()).isEqualTo( "Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]"); + assertThat(body.getTitle()).isEqualTo( + "Media type is not valid or not supported"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index dbdf0445919..ad8e1187eb7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -42,6 +42,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.stereotype.Controller; import org.springframework.validation.BindException; import org.springframework.validation.MapBindingResult; +import org.springframework.web.ErrorResponse; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -166,8 +167,11 @@ public class ResponseEntityExceptionHandlerTests { try { StaticMessageSource messageSource = new StaticMessageSource(); messageSource.addMessage( - "problemDetail." + HttpMediaTypeNotSupportedException.class.getName(), locale, + ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, null), locale, "Content-Type {0} not supported. Supported: {1}"); + messageSource.addMessage( + ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale, + "Media type is not valid or not supported"); this.exceptionHandler.setMessageSource(messageSource); @@ -177,6 +181,8 @@ public class ResponseEntityExceptionHandlerTests { ProblemDetail body = (ProblemDetail) entity.getBody(); assertThat(body.getDetail()).isEqualTo( "Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]"); + assertThat(body.getTitle()).isEqualTo( + "Media type is not valid or not supported"); } finally { LocaleContextHolder.resetLocaleContext(); @@ -201,7 +207,7 @@ public class ResponseEntityExceptionHandlerTests { try { StaticMessageSource messageSource = new StaticMessageSource(); messageSource.addMessage( - "problemDetail." + TypeMismatchException.class.getName(), locale, + ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null), locale, "Failed to set {0} to value: {1}"); this.exceptionHandler.setMessageSource(messageSource); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 1ead716f3dc..62e5e9da8ad 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -3614,7 +3614,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#<># You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response as follows: +any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -3626,8 +3626,9 @@ and also falls back on it if no compatible media type is found. To enable RFC 7807 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an <> in Spring configuration. The handler -obtains HTTP status, headers, and error details from each exception and prepares a -`ResponseEntity`. +has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which +includes all built-in web exceptions. You can add more exception handling methods, and +use a protected method to map any exception to a `ProblemDetail`. @@ -3644,7 +3645,7 @@ response, and likewise any unknown property during deserialization is inserted i this `Map`. You can also extend `ProblemDetail` to add dedicated non-standard properties. -The copy constructor in `ProblemDetail` allows a sub-class to make it easy to be created +The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created from an existing `ProblemDetail`. This could be done centrally, e.g. from an `@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the `ProblemDetail` of an exception into a subclass with the additional non-standard fields. @@ -3658,17 +3659,18 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an It is a common requirement to internationalize error response details, and good practice to customize the problem details for Spring WebFlux exceptions. This is supported as follows: -- Each `ErrorResponse` exposes a message code and message code arguments to resolve the -problem "detail" field through a <>. +- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field +through a <>. The actual message code value is parameterized with placeholders, e.g. `"HTTP method {0} not supported"` to be expanded from the arguments. -- `ResponseEntityExceptionHandler` uses the message code and the message arguments -to resolve the problem "detail" field. +- Each `ErrorResponse` also exposes a message code to resolve the "title" field. +- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the +"detail" and the "title" fields. -Message codes default to "problemDetail." + the fully qualified exception class name. Some -exceptions may expose additional message codes in which case a suffix is added to -the default message code. The table below lists message arguments and codes for Spring -WebFlux exceptions: +By default, the message code for the "detail" field is "problemDetail." + the fully +qualified exception class name. Some exceptions may expose additional message codes in +which case a suffix is added to the default message code. The table below lists message +arguments and codes for Spring WebFlux exceptions: [[webflux-ann-rest-exceptions-codes]] [cols="1,1,2", options="header"] @@ -3715,6 +3717,10 @@ via `MessageSource`. |=== +By default, the message code for the "title" field is "problemDetail.title." + the fully +qualified exception class name. + + [[webflux-ann-rest-exceptions-client]] diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 3c64109569a..c74c33b6c9c 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -4910,7 +4910,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#<># You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response as follows: +any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -4919,11 +4919,12 @@ already set. "application/problem+json" over "application/json" when rendering a `ProblemDetail`, and also falls back on it if no compatible media type is found. -To enable RFC 7807 responses for Spring MVC exceptions and for any +To enable RFC 7807 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an <> in Spring configuration. The handler -obtains HTTP status, headers, and error details from each exception and prepares a -`ResponseEntity`. +has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which +includes all built-in web exceptions. You can add more exception handling methods, and +use a protected method to map any exception to a `ProblemDetail`. @@ -4940,7 +4941,7 @@ response, and likewise any unknown property during deserialization is inserted i this `Map`. You can also extend `ProblemDetail` to add dedicated non-standard properties. -The copy constructor in `ProblemDetail` allows a sub-class to make it easy to be created +The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created from an existing `ProblemDetail`. This could be done centrally, e.g. from an `@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the `ProblemDetail` of an exception into a subclass with the additional non-standard fields. @@ -4954,20 +4955,18 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an It is a common requirement to internationalize error response details, and good practice to customize the problem details for Spring MVC exceptions. This is supported as follows: -- Each `ErrorResponse` exposes a message code and message code arguments to resolve the -problem "detail" field through a <>. +- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field +through a <>. The actual message code value is parameterized with placeholders, e.g. `"HTTP method {0} not supported"` to be expanded from the arguments. -- `ResponseEntityExceptionHandler` uses the message code and the message arguments -to resolve the problem "detail" field. -- Lower level exceptions that cannot implement `ErrorResponse`, e.g. `TypeMismatchException`, -have their problem detail, including message code and arguments set in -`ResponseEntityExceptionHandler`. +- Each `ErrorResponse` also exposes a message code to resolve the "title" field. +- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the +"detail" and the "title" fields. -Message codes default to "problemDetail." + the fully qualified exception class name. Some -exceptions may expose additional message codes in which case a suffix is added to -the default message code. The table below lists message arguments and codes for Spring -MVC exceptions: +By default, the message code for the "detail" field is "problemDetail." + the fully +qualified exception class name. Some exceptions may expose additional message codes in +which case a suffix is added to the default message code. The table below lists message +arguments and codes for Spring MVC exceptions: [[mvc-ann-rest-exceptions-codes]] [cols="1,1,2", options="header"] @@ -5054,6 +5053,9 @@ MVC exceptions: |=== +By default, the message code for the "title" field is "problemDetail.title." + the fully +qualified exception class name. + [[mvc-ann-rest-exceptions-client]]