Skip to content

Commit e71057d

Browse files
committed
Support i8n of ProblemDetail "title" field
Closes gh-29407
1 parent 506fbe5 commit e71057d

File tree

5 files changed

+80
-36
lines changed

5 files changed

+80
-36
lines changed

spring-web/src/main/java/org/springframework/web/ErrorResponse.java

+29-5
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,19 @@ default Object[] getDetailMessageArguments(MessageSource messageSource, Locale l
9595
}
9696

9797
/**
98-
* Resolve the {@link #getDetailMessageCode() detailMessageCode} through the
99-
* given {@link MessageSource}, and if found, update the "detail" field.
98+
* Return a code to use to resolve the problem "detail" for this exception
99+
* through a {@link MessageSource}.
100+
* <p>By default this is initialized via
101+
* {@link #getDefaultDetailMessageCode(Class, String)}.
102+
*/
103+
default String getTitleCode() {
104+
return getDefaultTitleMessageCode(getClass());
105+
}
106+
107+
/**
108+
* Resolve the {@link #getDetailMessageCode() detailMessageCode} and the
109+
* {@link #getTitleCode() titleCode} through the given {@link MessageSource},
110+
* and if found, update the "detail" and "title!" fields respectively.
100111
* @param messageSource the {@code MessageSource} to use for the lookup
101112
* @param locale the {@code Locale} to use for the lookup
102113
*/
@@ -107,22 +118,35 @@ default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Lo
107118
if (detail != null) {
108119
getBody().setDetail(detail);
109120
}
121+
String title = messageSource.getMessage(getTitleCode(), null, null, locale);
122+
if (title != null) {
123+
getBody().setTitle(title);
124+
}
110125
}
111126
return getBody();
112127
}
113128

114129

115130
/**
116-
* Build a message code for the given exception type, which consists of
117-
* {@code "problemDetail."} followed by the full {@link Class#getName() class name}.
118-
* @param exceptionType the exception type for which to build a code
131+
* Build a message code for the "detail" field, for the given exception type.
132+
* @param exceptionType the exception type associated with the problem
119133
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
120134
* error message with different arguments.
135+
* @return {@code "problemDetail."} followed by the full {@link Class#getName() class name}
121136
*/
122137
static String getDefaultDetailMessageCode(Class<?> exceptionType, @Nullable String suffix) {
123138
return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + suffix : "");
124139
}
125140

141+
/**
142+
* Build a message code for the "title" field, for the given exception type.
143+
* @param exceptionType the exception type associated with the problem
144+
* @return {@code "problemDetail.title."} followed by the full {@link Class#getName() class name}
145+
*/
146+
static String getDefaultTitleMessageCode(Class<?> exceptionType) {
147+
return "problemDetail.title." + exceptionType.getName();
148+
}
149+
126150
/**
127151
* Map the given Exception to an {@link ErrorResponse}.
128152
* @param ex the Exception, mostly to derive message codes, if not provided

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.http.ResponseEntity;
3737
import org.springframework.util.LinkedMultiValueMap;
3838
import org.springframework.validation.BeanPropertyBindingResult;
39+
import org.springframework.web.ErrorResponse;
3940
import org.springframework.web.ErrorResponseException;
4041
import org.springframework.web.bind.support.WebExchangeBindException;
4142
import org.springframework.web.server.MethodNotAllowedException;
@@ -131,8 +132,11 @@ void errorResponseProblemDetailViaMessageSource() {
131132

132133
StaticMessageSource messageSource = new StaticMessageSource();
133134
messageSource.addMessage(
134-
"problemDetail." + UnsupportedMediaTypeStatusException.class.getName(), locale,
135+
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, null), locale,
135136
"Content-Type {0} not supported. Supported: {1}");
137+
messageSource.addMessage(
138+
ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale,
139+
"Media type is not valid or not supported");
136140

137141
this.exceptionHandler.setMessageSource(messageSource);
138142

@@ -147,6 +151,8 @@ void errorResponseProblemDetailViaMessageSource() {
147151
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
148152
assertThat(body.getDetail()).isEqualTo(
149153
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
154+
assertThat(body.getTitle()).isEqualTo(
155+
"Media type is not valid or not supported");
150156
}
151157

152158
@Test

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.stereotype.Controller;
4343
import org.springframework.validation.BindException;
4444
import org.springframework.validation.MapBindingResult;
45+
import org.springframework.web.ErrorResponse;
4546
import org.springframework.web.HttpMediaTypeNotAcceptableException;
4647
import org.springframework.web.HttpMediaTypeNotSupportedException;
4748
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -166,8 +167,11 @@ public void errorResponseProblemDetailViaMessageSource() {
166167
try {
167168
StaticMessageSource messageSource = new StaticMessageSource();
168169
messageSource.addMessage(
169-
"problemDetail." + HttpMediaTypeNotSupportedException.class.getName(), locale,
170+
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, null), locale,
170171
"Content-Type {0} not supported. Supported: {1}");
172+
messageSource.addMessage(
173+
ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale,
174+
"Media type is not valid or not supported");
171175

172176
this.exceptionHandler.setMessageSource(messageSource);
173177

@@ -177,6 +181,8 @@ public void errorResponseProblemDetailViaMessageSource() {
177181
ProblemDetail body = (ProblemDetail) entity.getBody();
178182
assertThat(body.getDetail()).isEqualTo(
179183
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
184+
assertThat(body.getTitle()).isEqualTo(
185+
"Media type is not valid or not supported");
180186
}
181187
finally {
182188
LocaleContextHolder.resetLocaleContext();
@@ -201,7 +207,7 @@ public void typeMismatchWithProblemDetailViaMessageSource() {
201207
try {
202208
StaticMessageSource messageSource = new StaticMessageSource();
203209
messageSource.addMessage(
204-
"problemDetail." + TypeMismatchException.class.getName(), locale,
210+
ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null), locale,
205211
"Failed to set {0} to value: {1}");
206212

207213
this.exceptionHandler.setMessageSource(messageSource);

src/docs/asciidoc/web/webflux.adoc

+18-12
Original file line numberDiff line numberDiff line change
@@ -3614,7 +3614,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
36143614
[.small]#<<webmvc.adoc#mvc-ann-rest-exceptions-render, Web MVC>>#
36153615

36163616
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
3617-
any `@RequestMapping` method to render an RFC 7807 response as follows:
3617+
any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows:
36183618

36193619
- The `status` property of `ProblemDetail` determines the HTTP status.
36203620
- 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.
36263626
To enable RFC 7807 responses for Spring WebFlux exceptions and for any
36273627
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
36283628
<<webflux-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
3629-
obtains HTTP status, headers, and error details from each exception and prepares a
3630-
`ResponseEntity`.
3629+
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
3630+
includes all built-in web exceptions. You can add more exception handling methods, and
3631+
use a protected method to map any exception to a `ProblemDetail`.
36313632

36323633

36333634

@@ -3644,7 +3645,7 @@ response, and likewise any unknown property during deserialization is inserted i
36443645
this `Map`.
36453646

36463647
You can also extend `ProblemDetail` to add dedicated non-standard properties.
3647-
The copy constructor in `ProblemDetail` allows a sub-class to make it easy to be created
3648+
The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created
36483649
from an existing `ProblemDetail`. This could be done centrally, e.g. from an
36493650
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
36503651
`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
36583659
It is a common requirement to internationalize error response details, and good practice
36593660
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
36603661

3661-
- Each `ErrorResponse` exposes a message code and message code arguments to resolve the
3662-
problem "detail" field through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
3662+
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
3663+
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
36633664
The actual message code value is parameterized with placeholders, e.g.
36643665
`"HTTP method {0} not supported"` to be expanded from the arguments.
3665-
- `ResponseEntityExceptionHandler` uses the message code and the message arguments
3666-
to resolve the problem "detail" field.
3666+
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
3667+
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
3668+
"detail" and the "title" fields.
36673669

3668-
Message codes default to "problemDetail." + the fully qualified exception class name. Some
3669-
exceptions may expose additional message codes in which case a suffix is added to
3670-
the default message code. The table below lists message arguments and codes for Spring
3671-
WebFlux exceptions:
3670+
By default, the message code for the "detail" field is "problemDetail." + the fully
3671+
qualified exception class name. Some exceptions may expose additional message codes in
3672+
which case a suffix is added to the default message code. The table below lists message
3673+
arguments and codes for Spring WebFlux exceptions:
36723674

36733675
[[webflux-ann-rest-exceptions-codes]]
36743676
[cols="1,1,2", options="header"]
@@ -3715,6 +3717,10 @@ via `MessageSource`.
37153717

37163718
|===
37173719

3720+
By default, the message code for the "title" field is "problemDetail.title." + the fully
3721+
qualified exception class name.
3722+
3723+
37183724

37193725

37203726
[[webflux-ann-rest-exceptions-client]]

src/docs/asciidoc/web/webmvc.adoc

+18-16
Original file line numberDiff line numberDiff line change
@@ -4910,7 +4910,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
49104910
[.small]#<<web-reactive.adoc#webflux-ann-rest-exceptions-render, WebFlux>>#
49114911

49124912
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
4913-
any `@RequestMapping` method to render an RFC 7807 response as follows:
4913+
any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows:
49144914

49154915
- The `status` property of `ProblemDetail` determines the HTTP status.
49164916
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
@@ -4919,11 +4919,12 @@ already set.
49194919
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
49204920
and also falls back on it if no compatible media type is found.
49214921

4922-
To enable RFC 7807 responses for Spring MVC exceptions and for any
4922+
To enable RFC 7807 responses for Spring WebFlux exceptions and for any
49234923
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
49244924
<<mvc-ann-controller-advice,@ControllerAdvice>> in Spring configuration. The handler
4925-
obtains HTTP status, headers, and error details from each exception and prepares a
4926-
`ResponseEntity`.
4925+
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
4926+
includes all built-in web exceptions. You can add more exception handling methods, and
4927+
use a protected method to map any exception to a `ProblemDetail`.
49274928

49284929

49294930

@@ -4940,7 +4941,7 @@ response, and likewise any unknown property during deserialization is inserted i
49404941
this `Map`.
49414942

49424943
You can also extend `ProblemDetail` to add dedicated non-standard properties.
4943-
The copy constructor in `ProblemDetail` allows a sub-class to make it easy to be created
4944+
The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created
49444945
from an existing `ProblemDetail`. This could be done centrally, e.g. from an
49454946
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
49464947
`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
49544955
It is a common requirement to internationalize error response details, and good practice
49554956
to customize the problem details for Spring MVC exceptions. This is supported as follows:
49564957

4957-
- Each `ErrorResponse` exposes a message code and message code arguments to resolve the
4958-
problem "detail" field through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
4958+
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
4959+
through a <<core.adoc#context-functionality-messagesource,MessageSource>>.
49594960
The actual message code value is parameterized with placeholders, e.g.
49604961
`"HTTP method {0} not supported"` to be expanded from the arguments.
4961-
- `ResponseEntityExceptionHandler` uses the message code and the message arguments
4962-
to resolve the problem "detail" field.
4963-
- Lower level exceptions that cannot implement `ErrorResponse`, e.g. `TypeMismatchException`,
4964-
have their problem detail, including message code and arguments set in
4965-
`ResponseEntityExceptionHandler`.
4962+
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
4963+
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
4964+
"detail" and the "title" fields.
49664965

4967-
Message codes default to "problemDetail." + the fully qualified exception class name. Some
4968-
exceptions may expose additional message codes in which case a suffix is added to
4969-
the default message code. The table below lists message arguments and codes for Spring
4970-
MVC exceptions:
4966+
By default, the message code for the "detail" field is "problemDetail." + the fully
4967+
qualified exception class name. Some exceptions may expose additional message codes in
4968+
which case a suffix is added to the default message code. The table below lists message
4969+
arguments and codes for Spring MVC exceptions:
49714970

49724971
[[mvc-ann-rest-exceptions-codes]]
49734972
[cols="1,1,2", options="header"]
@@ -5054,6 +5053,9 @@ MVC exceptions:
50545053

50555054
|===
50565055

5056+
By default, the message code for the "title" field is "problemDetail.title." + the fully
5057+
qualified exception class name.
5058+
50575059

50585060

50595061
[[mvc-ann-rest-exceptions-client]]

0 commit comments

Comments
 (0)