Support i8n of ProblemDetail "title" field
Closes gh-29407
This commit is contained in:
parent
506fbe5243
commit
e71057dca9
|
|
@ -95,8 +95,19 @@ public interface ErrorResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the {@link #getDetailMessageCode() detailMessageCode} through the
|
* Return a code to use to resolve the problem "detail" for this exception
|
||||||
* given {@link MessageSource}, and if found, update the "detail" field.
|
* 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 messageSource the {@code MessageSource} to use for the lookup
|
||||||
* @param locale the {@code Locale} 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) {
|
if (detail != null) {
|
||||||
getBody().setDetail(detail);
|
getBody().setDetail(detail);
|
||||||
}
|
}
|
||||||
|
String title = messageSource.getMessage(getTitleCode(), null, null, locale);
|
||||||
|
if (title != null) {
|
||||||
|
getBody().setTitle(title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return getBody();
|
return getBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a message code for the given exception type, which consists of
|
* Build a message code for the "detail" field, for the given exception type.
|
||||||
* {@code "problemDetail."} followed by the full {@link Class#getName() class name}.
|
* @param exceptionType the exception type associated with the problem
|
||||||
* @param exceptionType the exception type for which to build a code
|
|
||||||
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
|
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
|
||||||
* error message with different arguments.
|
* 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) {
|
static String getDefaultDetailMessageCode(Class<?> exceptionType, @Nullable String suffix) {
|
||||||
return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + 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}.
|
* Map the given Exception to an {@link ErrorResponse}.
|
||||||
* @param ex the Exception, mostly to derive message codes, if not provided
|
* @param ex the Exception, mostly to derive message codes, if not provided
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import org.springframework.http.ProblemDetail;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.validation.BeanPropertyBindingResult;
|
import org.springframework.validation.BeanPropertyBindingResult;
|
||||||
|
import org.springframework.web.ErrorResponse;
|
||||||
import org.springframework.web.ErrorResponseException;
|
import org.springframework.web.ErrorResponseException;
|
||||||
import org.springframework.web.bind.support.WebExchangeBindException;
|
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||||
import org.springframework.web.server.MethodNotAllowedException;
|
import org.springframework.web.server.MethodNotAllowedException;
|
||||||
|
|
@ -131,8 +132,11 @@ public class ResponseEntityExceptionHandlerTests {
|
||||||
|
|
||||||
StaticMessageSource messageSource = new StaticMessageSource();
|
StaticMessageSource messageSource = new StaticMessageSource();
|
||||||
messageSource.addMessage(
|
messageSource.addMessage(
|
||||||
"problemDetail." + UnsupportedMediaTypeStatusException.class.getName(), locale,
|
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, null), locale,
|
||||||
"Content-Type {0} not supported. Supported: {1}");
|
"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);
|
this.exceptionHandler.setMessageSource(messageSource);
|
||||||
|
|
||||||
|
|
@ -147,6 +151,8 @@ public class ResponseEntityExceptionHandlerTests {
|
||||||
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
|
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
|
||||||
assertThat(body.getDetail()).isEqualTo(
|
assertThat(body.getDetail()).isEqualTo(
|
||||||
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
|
"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
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.MapBindingResult;
|
import org.springframework.validation.MapBindingResult;
|
||||||
|
import org.springframework.web.ErrorResponse;
|
||||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
|
@ -166,8 +167,11 @@ public class ResponseEntityExceptionHandlerTests {
|
||||||
try {
|
try {
|
||||||
StaticMessageSource messageSource = new StaticMessageSource();
|
StaticMessageSource messageSource = new StaticMessageSource();
|
||||||
messageSource.addMessage(
|
messageSource.addMessage(
|
||||||
"problemDetail." + HttpMediaTypeNotSupportedException.class.getName(), locale,
|
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, null), locale,
|
||||||
"Content-Type {0} not supported. Supported: {1}");
|
"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);
|
this.exceptionHandler.setMessageSource(messageSource);
|
||||||
|
|
||||||
|
|
@ -177,6 +181,8 @@ public class ResponseEntityExceptionHandlerTests {
|
||||||
ProblemDetail body = (ProblemDetail) entity.getBody();
|
ProblemDetail body = (ProblemDetail) entity.getBody();
|
||||||
assertThat(body.getDetail()).isEqualTo(
|
assertThat(body.getDetail()).isEqualTo(
|
||||||
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
|
"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 {
|
finally {
|
||||||
LocaleContextHolder.resetLocaleContext();
|
LocaleContextHolder.resetLocaleContext();
|
||||||
|
|
@ -201,7 +207,7 @@ public class ResponseEntityExceptionHandlerTests {
|
||||||
try {
|
try {
|
||||||
StaticMessageSource messageSource = new StaticMessageSource();
|
StaticMessageSource messageSource = new StaticMessageSource();
|
||||||
messageSource.addMessage(
|
messageSource.addMessage(
|
||||||
"problemDetail." + TypeMismatchException.class.getName(), locale,
|
ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null), locale,
|
||||||
"Failed to set {0} to value: {1}");
|
"Failed to set {0} to value: {1}");
|
||||||
|
|
||||||
this.exceptionHandler.setMessageSource(messageSource);
|
this.exceptionHandler.setMessageSource(messageSource);
|
||||||
|
|
|
||||||
|
|
@ -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>>#
|
[.small]#<<webmvc.adoc#mvc-ann-rest-exceptions-render, Web MVC>>#
|
||||||
|
|
||||||
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
|
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 `status` property of `ProblemDetail` determines the HTTP status.
|
||||||
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
|
- 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
|
To enable RFC 7807 responses for Spring WebFlux exceptions and for any
|
||||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||||
<<webflux-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
|
<<webflux-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
|
||||||
obtains HTTP status, headers, and error details from each exception and prepares a
|
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
|
||||||
`ResponseEntity`.
|
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`.
|
this `Map`.
|
||||||
|
|
||||||
You can also extend `ProblemDetail` to add dedicated non-standard properties.
|
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
|
from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||||
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
|
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
|
||||||
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
|
`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
|
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:
|
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
|
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||||
problem "detail" field through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
|
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
|
||||||
The actual message code value is parameterized with placeholders, e.g.
|
The actual message code value is parameterized with placeholders, e.g.
|
||||||
`"HTTP method {0} not supported"` to be expanded from the arguments.
|
`"HTTP method {0} not supported"` to be expanded from the arguments.
|
||||||
- `ResponseEntityExceptionHandler` uses the message code and the message arguments
|
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||||
to resolve the problem "detail" 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
|
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||||
exceptions may expose additional message codes in which case a suffix is added to
|
qualified exception class name. Some exceptions may expose additional message codes in
|
||||||
the default message code. The table below lists message arguments and codes for Spring
|
which case a suffix is added to the default message code. The table below lists message
|
||||||
WebFlux exceptions:
|
arguments and codes for Spring WebFlux exceptions:
|
||||||
|
|
||||||
[[webflux-ann-rest-exceptions-codes]]
|
[[webflux-ann-rest-exceptions-codes]]
|
||||||
[cols="1,1,2", options="header"]
|
[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]]
|
[[webflux-ann-rest-exceptions-client]]
|
||||||
|
|
|
||||||
|
|
@ -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>>#
|
[.small]#<<web-reactive.adoc#webflux-ann-rest-exceptions-render, WebFlux>>#
|
||||||
|
|
||||||
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
|
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 `status` property of `ProblemDetail` determines the HTTP status.
|
||||||
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
|
- 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`,
|
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
|
||||||
and also falls back on it if no compatible media type is found.
|
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
|
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||||
<<mvc-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
|
<<mvc-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
|
||||||
obtains HTTP status, headers, and error details from each exception and prepares a
|
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
|
||||||
`ResponseEntity`.
|
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`.
|
this `Map`.
|
||||||
|
|
||||||
You can also extend `ProblemDetail` to add dedicated non-standard properties.
|
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
|
from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||||
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
|
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
|
||||||
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
|
`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
|
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:
|
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
|
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||||
problem "detail" field through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
|
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
|
||||||
The actual message code value is parameterized with placeholders, e.g.
|
The actual message code value is parameterized with placeholders, e.g.
|
||||||
`"HTTP method {0} not supported"` to be expanded from the arguments.
|
`"HTTP method {0} not supported"` to be expanded from the arguments.
|
||||||
- `ResponseEntityExceptionHandler` uses the message code and the message arguments
|
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||||
to resolve the problem "detail" field.
|
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
|
||||||
- Lower level exceptions that cannot implement `ErrorResponse`, e.g. `TypeMismatchException`,
|
"detail" and the "title" fields.
|
||||||
have their problem detail, including message code and arguments set in
|
|
||||||
`ResponseEntityExceptionHandler`.
|
|
||||||
|
|
||||||
Message codes default to "problemDetail." + the fully qualified exception class name. Some
|
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||||
exceptions may expose additional message codes in which case a suffix is added to
|
qualified exception class name. Some exceptions may expose additional message codes in
|
||||||
the default message code. The table below lists message arguments and codes for Spring
|
which case a suffix is added to the default message code. The table below lists message
|
||||||
MVC exceptions:
|
arguments and codes for Spring MVC exceptions:
|
||||||
|
|
||||||
[[mvc-ann-rest-exceptions-codes]]
|
[[mvc-ann-rest-exceptions-codes]]
|
||||||
[cols="1,1,2", options="header"]
|
[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]]
|
[[mvc-ann-rest-exceptions-client]]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue