Skip to content

Commit cde9166

Browse files
committed
Merge branch '3.2.x'
Closes gh-40474
2 parents 9184448 + 1f06aa2 commit cde9166

File tree

4 files changed

+134
-46
lines changed

4 files changed

+134
-46
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.StringWriter;
2121
import java.util.Date;
2222
import java.util.LinkedHashMap;
23+
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
2526

@@ -32,6 +33,7 @@
3233
import org.springframework.util.StringUtils;
3334
import org.springframework.validation.BindingResult;
3435
import org.springframework.validation.ObjectError;
36+
import org.springframework.validation.method.MethodValidationResult;
3537
import org.springframework.web.bind.annotation.ResponseStatus;
3638
import org.springframework.web.reactive.function.server.ServerRequest;
3739
import org.springframework.web.server.ResponseStatusException;
@@ -46,8 +48,8 @@
4648
* <li>error - The error reason</li>
4749
* <li>exception - The class name of the root exception (if configured)</li>
4850
* <li>message - The exception message (if configured)</li>
49-
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception (if
50-
* configured)</li>
51+
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
52+
* {@link MethodValidationResult} exception (if configured)</li>
5153
* <li>trace - The exception stack trace (if configured)</li>
5254
* <li>path - The URL path when the exception was raised</li>
5355
* <li>requestId - Unique ID associated with the current request</li>
@@ -58,6 +60,7 @@
5860
* @author Michele Mancioppi
5961
* @author Scott Frederick
6062
* @author Moritz Halbritter
63+
* @author Yanming Zhou
6164
* @since 2.0.0
6265
* @see ErrorAttributes
6366
*/
@@ -97,9 +100,8 @@ private Map<String, Object> getErrorAttributes(ServerRequest request, boolean in
97100
HttpStatus errorStatus = determineHttpStatus(error, responseStatusAnnotation);
98101
errorAttributes.put("status", errorStatus.value());
99102
errorAttributes.put("error", errorStatus.getReasonPhrase());
100-
errorAttributes.put("message", determineMessage(error, responseStatusAnnotation));
101103
errorAttributes.put("requestId", request.exchange().getRequest().getId());
102-
handleException(errorAttributes, determineException(error), includeStackTrace);
104+
handleException(errorAttributes, error, responseStatusAnnotation, includeStackTrace);
103105
return errorAttributes;
104106
}
105107

@@ -113,44 +115,51 @@ private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<Respons
113115
return responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
114116
}
115117

116-
private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
117-
if (error instanceof BindingResult) {
118-
return error.getMessage();
119-
}
120-
if (error instanceof ResponseStatusException responseStatusException) {
121-
return responseStatusException.getReason();
122-
}
123-
String reason = responseStatusAnnotation.getValue("reason", String.class).orElse("");
124-
if (StringUtils.hasText(reason)) {
125-
return reason;
126-
}
127-
return (error.getMessage() != null) ? error.getMessage() : "";
128-
}
129-
130-
private Throwable determineException(Throwable error) {
131-
if (error instanceof ResponseStatusException) {
132-
return (error.getCause() != null) ? error.getCause() : error;
133-
}
134-
return error;
135-
}
136-
137118
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
138119
StringWriter stackTrace = new StringWriter();
139120
error.printStackTrace(new PrintWriter(stackTrace));
140121
stackTrace.flush();
141122
errorAttributes.put("trace", stackTrace.toString());
142123
}
143124

144-
private void handleException(Map<String, Object> errorAttributes, Throwable error, boolean includeStackTrace) {
145-
errorAttributes.put("exception", error.getClass().getName());
146-
if (includeStackTrace) {
147-
addStackTrace(errorAttributes, error);
125+
private void handleException(Map<String, Object> errorAttributes, Throwable error,
126+
MergedAnnotation<ResponseStatus> responseStatusAnnotation, boolean includeStackTrace) {
127+
Throwable exception;
128+
if (error instanceof BindingResult bindingResult) {
129+
errorAttributes.put("message", error.getMessage());
130+
errorAttributes.put("errors", bindingResult.getAllErrors());
131+
exception = error;
148132
}
149-
if (error instanceof BindingResult result) {
150-
if (result.hasErrors()) {
151-
errorAttributes.put("errors", result.getAllErrors());
152-
}
133+
else if (error instanceof MethodValidationResult methodValidationResult) {
134+
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
135+
exception = error;
136+
}
137+
else if (error instanceof ResponseStatusException responseStatusException) {
138+
errorAttributes.put("message", responseStatusException.getReason());
139+
exception = (responseStatusException.getCause() != null) ? responseStatusException.getCause() : error;
140+
}
141+
else {
142+
exception = error;
143+
String reason = responseStatusAnnotation.getValue("reason", String.class).orElse("");
144+
String message = StringUtils.hasText(reason) ? reason : error.getMessage();
145+
errorAttributes.put("message", (message != null) ? message : "");
153146
}
147+
errorAttributes.put("exception", exception.getClass().getName());
148+
if (includeStackTrace) {
149+
addStackTrace(errorAttributes, exception);
150+
}
151+
}
152+
153+
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
154+
MethodValidationResult result) {
155+
List<ObjectError> errors = result.getAllErrors()
156+
.stream()
157+
.filter(ObjectError.class::isInstance)
158+
.map(ObjectError.class::cast)
159+
.toList();
160+
errorAttributes.put("message",
161+
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
162+
errorAttributes.put("errors", errors);
154163
}
155164

156165
@Override

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.StringWriter;
2121
import java.util.Date;
2222
import java.util.LinkedHashMap;
23+
import java.util.List;
2324
import java.util.Map;
2425

2526
import jakarta.servlet.RequestDispatcher;
@@ -36,6 +37,7 @@
3637
import org.springframework.util.StringUtils;
3738
import org.springframework.validation.BindingResult;
3839
import org.springframework.validation.ObjectError;
40+
import org.springframework.validation.method.MethodValidationResult;
3941
import org.springframework.web.context.request.RequestAttributes;
4042
import org.springframework.web.context.request.WebRequest;
4143
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -50,8 +52,8 @@
5052
* <li>error - The error reason</li>
5153
* <li>exception - The class name of the root exception (if configured)</li>
5254
* <li>message - The exception message (if configured)</li>
53-
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception (if
54-
* configured)</li>
55+
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
56+
* {@link MethodValidationResult} exception (if configured)</li>
5557
* <li>trace - The exception stack trace (if configured)</li>
5658
* <li>path - The URL path when the exception was raised</li>
5759
* </ul>
@@ -62,6 +64,7 @@
6264
* @author Vedran Pavic
6365
* @author Scott Frederick
6466
* @author Moritz Halbritter
67+
* @author Yanming Zhou
6568
* @since 2.0.0
6669
* @see ErrorAttributes
6770
*/
@@ -149,12 +152,18 @@ private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest web
149152
}
150153

151154
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
152-
BindingResult result = extractBindingResult(error);
153-
if (result == null) {
154-
addExceptionErrorMessage(errorAttributes, webRequest, error);
155+
BindingResult bindingResult = extractBindingResult(error);
156+
if (bindingResult != null) {
157+
addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult);
155158
}
156159
else {
157-
addBindingResultErrorMessage(errorAttributes, result);
160+
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
161+
if (methodValidationResult != null) {
162+
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
163+
}
164+
else {
165+
addExceptionErrorMessage(errorAttributes, webRequest, error);
166+
}
158167
}
159168
}
160169

@@ -187,10 +196,25 @@ protected String getMessage(WebRequest webRequest, Throwable error) {
187196
return "No message available";
188197
}
189198

190-
private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result) {
191-
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. "
192-
+ "Error count: " + result.getErrorCount());
193-
errorAttributes.put("errors", result.getAllErrors());
199+
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
200+
addMessageAndErrorsForValidationFailure(errorAttributes, "object='" + result.getObjectName() + "'",
201+
result.getAllErrors());
202+
}
203+
204+
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
205+
MethodValidationResult result) {
206+
List<ObjectError> errors = result.getAllErrors()
207+
.stream()
208+
.filter(ObjectError.class::isInstance)
209+
.map(ObjectError.class::cast)
210+
.toList();
211+
addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors);
212+
}
213+
214+
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
215+
List<ObjectError> errors) {
216+
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());
217+
errorAttributes.put("errors", errors);
194218
}
195219

196220
private BindingResult extractBindingResult(Throwable error) {
@@ -200,6 +224,13 @@ private BindingResult extractBindingResult(Throwable error) {
200224
return null;
201225
}
202226

227+
private MethodValidationResult extractMethodValidationResult(Throwable error) {
228+
if (error instanceof MethodValidationResult methodValidationResult) {
229+
return methodValidationResult;
230+
}
231+
return null;
232+
}
233+
203234
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
204235
StringWriter stackTrace = new StringWriter();
205236
error.printStackTrace(new PrintWriter(stackTrace));

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535
import org.springframework.validation.BindingResult;
3636
import org.springframework.validation.MapBindingResult;
3737
import org.springframework.validation.ObjectError;
38+
import org.springframework.validation.method.MethodValidationResult;
39+
import org.springframework.validation.method.ParameterValidationResult;
3840
import org.springframework.web.bind.annotation.ResponseStatus;
3941
import org.springframework.web.bind.support.WebExchangeBindException;
42+
import org.springframework.web.method.annotation.HandlerMethodValidationException;
4043
import org.springframework.web.reactive.function.server.ServerRequest;
4144
import org.springframework.web.server.ResponseStatusException;
4245
import org.springframework.web.server.ServerWebExchange;
@@ -51,6 +54,7 @@
5154
* @author Stephane Nicoll
5255
* @author Scott Frederick
5356
* @author Moritz Halbritter
57+
* @author Yanming Zhou
5458
*/
5559
class DefaultErrorAttributesTests {
5660

@@ -271,6 +275,25 @@ void extractBindingResultErrors() throws Exception {
271275
assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors());
272276
}
273277

278+
@Test
279+
void extractMethodValidationResultErrors() throws Exception {
280+
Object target = "test";
281+
Method method = String.class.getMethod("substring", int.class);
282+
MethodParameter parameter = new MethodParameter(method, 0);
283+
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
284+
List.of(new ParameterValidationResult(parameter, -1,
285+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null)));
286+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
287+
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
288+
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
289+
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
290+
assertThat(attributes.get("message")).asString()
291+
.isEqualTo(
292+
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
293+
assertThat(attributes).containsEntry("errors",
294+
methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
295+
}
296+
274297
@Test
275298
void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
276299
Method method = getClass().getDeclaredMethod("method", String.class);

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import java.lang.reflect.Method;
2020
import java.util.Collections;
2121
import java.util.Date;
22+
import java.util.List;
2223
import java.util.Map;
2324

2425
import jakarta.servlet.ServletException;
2526
import org.junit.jupiter.api.Test;
2627

2728
import org.springframework.boot.web.error.ErrorAttributeOptions;
2829
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
30+
import org.springframework.context.MessageSourceResolvable;
2931
import org.springframework.core.MethodParameter;
3032
import org.springframework.http.HttpStatus;
3133
import org.springframework.mock.web.MockHttpServletRequest;
@@ -34,9 +36,12 @@
3436
import org.springframework.validation.BindingResult;
3537
import org.springframework.validation.MapBindingResult;
3638
import org.springframework.validation.ObjectError;
39+
import org.springframework.validation.method.MethodValidationResult;
40+
import org.springframework.validation.method.ParameterValidationResult;
3741
import org.springframework.web.bind.MethodArgumentNotValidException;
3842
import org.springframework.web.context.request.ServletWebRequest;
3943
import org.springframework.web.context.request.WebRequest;
44+
import org.springframework.web.method.annotation.HandlerMethodValidationException;
4045
import org.springframework.web.servlet.ModelAndView;
4146

4247
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,6 +53,7 @@
4853
* @author Vedran Pavic
4954
* @author Scott Frederick
5055
* @author Moritz Halbritter
56+
* @author Yanming Zhou
5157
*/
5258
class DefaultErrorAttributesTests {
5359

@@ -202,18 +208,37 @@ void withMethodArgumentNotValidExceptionBindingErrors() {
202208
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
203209
}
204210

211+
@Test
212+
void withHandlerMethodValidationExceptionBindingErrors() {
213+
Object target = "test";
214+
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
215+
MethodParameter parameter = new MethodParameter(method, 0);
216+
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
217+
List.of(new ParameterValidationResult(parameter, -1,
218+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null)));
219+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
220+
testErrors(methodValidationResult.getAllErrors(),
221+
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1",
222+
ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
223+
}
224+
205225
private void testBindingResult(BindingResult bindingResult, Exception ex, ErrorAttributeOptions options) {
226+
testErrors(bindingResult.getAllErrors(), "Validation failed for object='objectName'. Error count: 1", ex,
227+
options);
228+
}
229+
230+
private void testErrors(List<? extends MessageSourceResolvable> errors, String expectedMessage, Exception ex,
231+
ErrorAttributeOptions options) {
206232
this.request.setAttribute("jakarta.servlet.error.exception", ex);
207233
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest, options);
208234
if (options.isIncluded(Include.MESSAGE)) {
209-
assertThat(attributes).containsEntry("message",
210-
"Validation failed for object='objectName'. Error count: 1");
235+
assertThat(attributes).containsEntry("message", expectedMessage);
211236
}
212237
else {
213238
assertThat(attributes).doesNotContainKey("message");
214239
}
215240
if (options.isIncluded(Include.BINDING_ERRORS)) {
216-
assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors());
241+
assertThat(attributes).containsEntry("errors", errors);
217242
}
218243
else {
219244
assertThat(attributes).doesNotContainKey("errors");

0 commit comments

Comments
 (0)