Skip to content

Commit 9c7b5cb

Browse files
committed
Add ProblemDetail "type" message code
See gh-30566
1 parent 53828cb commit 9c7b5cb

File tree

5 files changed

+78
-44
lines changed

5 files changed

+78
-44
lines changed

framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
7171
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]#
7272

7373
It is a common requirement to internationalize error response details, and good practice
74-
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
74+
to customize the problem details for Spring WebFlux exceptions. This section describes the
75+
support for that.
7576

76-
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
77-
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
78-
The actual message code value is parameterized with placeholders, e.g.
79-
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
80-
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
81-
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
82-
"detail" and the "title" fields.
77+
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
78+
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
79+
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
80+
and updates the `ProblemDetail` accordingly.
8381

84-
By default, the message code for the "detail" field is "problemDetail." + the fully
85-
qualified exception class name. Some exceptions may expose additional message codes in
86-
which case a suffix is added to the default message code. The table below lists message
87-
arguments and codes for Spring WebFlux exceptions:
82+
The default strategy for message codes follows the pattern:
83+
84+
`problemDetail.[type|title|detail].[fully qualified exception class name]`
85+
86+
Some `ErrorResponse` may expose more than one message code, typically adding a suffix
87+
to the default message code. The table below lists message codes, and arguments for
88+
Spring WebFlux exceptions:
8889

8990
[[webflux-ann-rest-exceptions-codes]]
9091
[cols="1,1,2", options="header"]
@@ -131,9 +132,6 @@ via `MessageSource`.
131132

132133
|===
133134

134-
By default, the message code for the "title" field is "problemDetail.title." + the fully
135-
qualified exception class name.
136-
137135

138136

139137

framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
7171
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]#
7272

7373
It is a common requirement to internationalize error response details, and good practice
74-
to customize the problem details for Spring MVC exceptions. This is supported as follows:
74+
to customize the problem details for Spring WebFlux exceptions. This section describes the
75+
support for that.
7576

76-
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
77-
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
78-
The actual message code value is parameterized with placeholders, e.g.
79-
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
80-
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
81-
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
82-
"detail" and the "title" fields.
77+
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
78+
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
79+
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
80+
and updates the `ProblemDetail` accordingly.
8381

84-
By default, the message code for the "detail" field is "problemDetail." + the fully
85-
qualified exception class name. Some exceptions may expose additional message codes in
86-
which case a suffix is added to the default message code. The table below lists message
87-
arguments and codes for Spring MVC exceptions:
82+
The default strategy for message codes follows the pattern:
83+
84+
`problemDetail.[type|title|detail].[fully qualified exception class name]`
85+
86+
Some `ErrorResponse` may expose more than one message code, typically adding a suffix
87+
to the default message code. The table below lists message codes, and arguments for
88+
Spring MVC exceptions:
8889

8990
[[mvc-ann-rest-exceptions-codes]]
9091
[cols="1,1,2", options="header"]
@@ -171,8 +172,6 @@ arguments and codes for Spring MVC exceptions:
171172

172173
|===
173174

174-
By default, the message code for the "title" field is "problemDetail.title." + the fully
175-
qualified exception class name.
176175

177176

178177

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ default HttpHeaders getHeaders() {
6464
*/
6565
ProblemDetail getBody();
6666

67+
/**
68+
* Return a code to use to resolve the problem "type" for this exception
69+
* through a {@link MessageSource}. The type resolved through the
70+
* {@code MessageSource} will be passed into {@link URI#create(String)}
71+
* and therefore must be an encoded URI String.
72+
* <p>By default this is initialized via {@link #getDefaultTypeMessageCode(Class)}.
73+
* @since 6.1
74+
*/
75+
default String getTypeMessageCode() {
76+
return getDefaultTypeMessageCode(getClass());
77+
}
78+
6779
/**
6880
* Return a code to use to resolve the problem "detail" for this exception
6981
* through a {@link MessageSource}.
@@ -109,15 +121,19 @@ default String getTitleMessageCode() {
109121
}
110122

111123
/**
112-
* Resolve the {@link #getDetailMessageCode() detailMessageCode} and the
113-
* {@link #getTitleMessageCode() titleMessageCode} through the given
114-
* {@link MessageSource}, and if found, update the "detail" and "title"
115-
* fields respectively.
124+
* Use the given {@link MessageSource} to resolve the
125+
* {@link #getTypeMessageCode() type}, {@link #getTitleMessageCode() title},
126+
* and {@link #getDetailMessageCode() detail} message codes, and then use the
127+
* resolved values to update the corresponding fields in {@link #getBody()}.
116128
* @param messageSource the {@code MessageSource} to use for the lookup
117129
* @param locale the {@code Locale} to use for the lookup
118130
*/
119131
default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Locale locale) {
120132
if (messageSource != null) {
133+
String type = messageSource.getMessage(getTypeMessageCode(), null, null, locale);
134+
if (type != null) {
135+
getBody().setType(URI.create(type));
136+
}
121137
Object[] arguments = getDetailMessageArguments(messageSource, locale);
122138
String detail = messageSource.getMessage(getDetailMessageCode(), arguments, null, locale);
123139
if (detail != null) {
@@ -132,6 +148,17 @@ default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Lo
132148
}
133149

134150

151+
/**
152+
* Build a message code for the "type" field, for the given exception type.
153+
* @param exceptionType the exception type associated with the problem
154+
* @return {@code "problemDetail.type."} followed by the fully qualified
155+
* {@link Class#getName() class name}
156+
* @since 6.1
157+
*/
158+
static String getDefaultTypeMessageCode(Class<?> exceptionType) {
159+
return "problemDetail.type." + exceptionType.getName();
160+
}
161+
135162
/**
136163
* Build a message code for the "detail" field, for the given exception type.
137164
* @param exceptionType the exception type associated with the problem

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
import java.lang.reflect.Method;
21+
import java.net.URI;
2122
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.Locale;
@@ -130,29 +131,33 @@ void errorResponseProblemDetailViaMessageSource() {
130131
Locale locale = Locale.UK;
131132
LocaleContextHolder.setLocale(locale);
132133

134+
String type = "https://example.com/probs/unsupported-content";
135+
String title = "Media type is not valid or not supported";
136+
133137
StaticMessageSource messageSource = new StaticMessageSource();
134138
messageSource.addMessage(
135139
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, null), locale,
136140
"Content-Type {0} not supported. Supported: {1}");
137141
messageSource.addMessage(
138-
ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale,
139-
"Media type is not valid or not supported");
142+
ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale, title);
143+
messageSource.addMessage(
144+
ErrorResponse.getDefaultTypeMessageCode(UnsupportedMediaTypeStatusException.class), locale, type);
140145

141146
this.exceptionHandler.setMessageSource(messageSource);
142147

143148
Exception ex = new UnsupportedMediaTypeStatusException(MediaType.APPLICATION_JSON,
144149
List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML));
145150

146-
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")
147-
.acceptLanguageAsLocales(locale).build());
151+
MockServerWebExchange exchange = MockServerWebExchange.from(
152+
MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build());
148153

149154
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(ex, exchange).block();
150155

151156
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
152157
assertThat(body.getDetail()).isEqualTo(
153158
"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");
159+
assertThat(body.getTitle()).isEqualTo(title);
160+
assertThat(body.getType()).isEqualTo(URI.create(type));
156161
}
157162

158163
@Test

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

1919
import java.beans.PropertyChangeEvent;
20+
import java.net.URI;
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.List;
@@ -164,14 +165,18 @@ public void errorResponseProblemDetailViaMessageSource() {
164165
Locale locale = Locale.UK;
165166
LocaleContextHolder.setLocale(locale);
166167

168+
String type = "https://example.com/probs/unsupported-content";
169+
String title = "Media type is not valid or not supported";
170+
167171
try {
168172
StaticMessageSource messageSource = new StaticMessageSource();
169173
messageSource.addMessage(
170174
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, null), locale,
171175
"Content-Type {0} not supported. Supported: {1}");
172176
messageSource.addMessage(
173-
ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale,
174-
"Media type is not valid or not supported");
177+
ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale, title);
178+
messageSource.addMessage(
179+
ErrorResponse.getDefaultTypeMessageCode(HttpMediaTypeNotSupportedException.class), locale, type);
175180

176181
this.exceptionHandler.setMessageSource(messageSource);
177182

@@ -181,8 +186,8 @@ public void errorResponseProblemDetailViaMessageSource() {
181186
ProblemDetail body = (ProblemDetail) entity.getBody();
182187
assertThat(body.getDetail()).isEqualTo(
183188
"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");
189+
assertThat(body.getTitle()).isEqualTo(title);
190+
assertThat(body.getType()).isEqualTo(URI.create(type));
186191
}
187192
finally {
188193
LocaleContextHolder.resetLocaleContext();

0 commit comments

Comments
 (0)