Support i8n of ProblemDetail "title" field

Closes gh-29407
This commit is contained in:
rstoyanchev 2022-11-01 12:41:09 +00:00
parent 506fbe5243
commit e71057dca9
5 changed files with 80 additions and 36 deletions

View File

@ -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}.
* <p>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

View File

@ -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

View File

@ -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);

View File

@ -3614,7 +3614,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
[.small]#<<webmvc.adoc#mvc-ann-rest-exceptions-render, Web MVC>>#
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
<<webflux-ann-controller-advice,@ControllerAdvice>> 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 <<core.adoc#context-functionality-messagesource,MessageSource>>.
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
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]]

View File

@ -4910,7 +4910,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
[.small]#<<web-reactive.adoc#webflux-ann-rest-exceptions-render, WebFlux>>#
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
<<mvc-ann-controller-advice,@ControllerAdvice>> 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 <<core.adoc#context-functionality-messagesource,MessageSource>>.
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
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]]