Skip to content

Commit 1f06aa2

Browse files
committed
Merge pull request #39865 from quaff
* gh-39865: Polish "Consider HandlerMethodValidationException in DefaultErrorAttributes" Consider HandlerMethodValidationException in DefaultErrorAttributes Closes gh-39865
2 parents 4b61ae4 + 82b2189 commit 1f06aa2

File tree

4 files changed

+138
-50
lines changed

4 files changed

+138
-50
lines changed

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

+43-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -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>
@@ -57,6 +59,7 @@
5759
* @author Stephane Nicoll
5860
* @author Michele Mancioppi
5961
* @author Scott Frederick
62+
* @author Yanming Zhou
6063
* @since 2.0.0
6164
* @see ErrorAttributes
6265
*/
@@ -93,9 +96,8 @@ private Map<String, Object> getErrorAttributes(ServerRequest request, boolean in
9396
HttpStatus errorStatus = determineHttpStatus(error, responseStatusAnnotation);
9497
errorAttributes.put("status", errorStatus.value());
9598
errorAttributes.put("error", errorStatus.getReasonPhrase());
96-
errorAttributes.put("message", determineMessage(error, responseStatusAnnotation));
9799
errorAttributes.put("requestId", request.exchange().getRequest().getId());
98-
handleException(errorAttributes, determineException(error), includeStackTrace);
100+
handleException(errorAttributes, error, responseStatusAnnotation, includeStackTrace);
99101
return errorAttributes;
100102
}
101103

@@ -109,44 +111,51 @@ private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<Respons
109111
return responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
110112
}
111113

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

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

152161
@Override

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

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -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>
@@ -61,6 +63,7 @@
6163
* @author Stephane Nicoll
6264
* @author Vedran Pavic
6365
* @author Scott Frederick
66+
* @author Yanming Zhou
6467
* @since 2.0.0
6568
* @see ErrorAttributes
6669
*/
@@ -145,12 +148,18 @@ private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest web
145148
}
146149

147150
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
148-
BindingResult result = extractBindingResult(error);
149-
if (result == null) {
150-
addExceptionErrorMessage(errorAttributes, webRequest, error);
151+
BindingResult bindingResult = extractBindingResult(error);
152+
if (bindingResult != null) {
153+
addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult);
151154
}
152155
else {
153-
addBindingResultErrorMessage(errorAttributes, result);
156+
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
157+
if (methodValidationResult != null) {
158+
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
159+
}
160+
else {
161+
addExceptionErrorMessage(errorAttributes, webRequest, error);
162+
}
154163
}
155164
}
156165

@@ -183,10 +192,25 @@ protected String getMessage(WebRequest webRequest, Throwable error) {
183192
return "No message available";
184193
}
185194

186-
private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result) {
187-
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. "
188-
+ "Error count: " + result.getErrorCount());
189-
errorAttributes.put("errors", result.getAllErrors());
195+
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
196+
addMessageAndErrorsForValidationFailure(errorAttributes, "object='" + result.getObjectName() + "'",
197+
result.getAllErrors());
198+
}
199+
200+
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
201+
MethodValidationResult result) {
202+
List<ObjectError> errors = result.getAllErrors()
203+
.stream()
204+
.filter(ObjectError.class::isInstance)
205+
.map(ObjectError.class::cast)
206+
.toList();
207+
addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors);
208+
}
209+
210+
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
211+
List<ObjectError> errors) {
212+
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());
213+
errorAttributes.put("errors", errors);
190214
}
191215

192216
private BindingResult extractBindingResult(Throwable error) {
@@ -196,6 +220,13 @@ private BindingResult extractBindingResult(Throwable error) {
196220
return null;
197221
}
198222

223+
private MethodValidationResult extractMethodValidationResult(Throwable error) {
224+
if (error instanceof MethodValidationResult methodValidationResult) {
225+
return methodValidationResult;
226+
}
227+
return null;
228+
}
229+
199230
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
200231
StringWriter stackTrace = new StringWriter();
201232
error.printStackTrace(new PrintWriter(stackTrace));

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -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;
@@ -50,6 +53,7 @@
5053
* @author Brian Clozel
5154
* @author Stephane Nicoll
5255
* @author Scott Frederick
56+
* @author Yanming Zhou
5357
*/
5458
class DefaultErrorAttributesTests {
5559

@@ -246,6 +250,25 @@ void extractBindingResultErrors() throws Exception {
246250
assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors());
247251
}
248252

253+
@Test
254+
void extractMethodValidationResultErrors() throws Exception {
255+
Object target = "test";
256+
Method method = String.class.getMethod("substring", int.class);
257+
MethodParameter parameter = new MethodParameter(method, 0);
258+
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
259+
List.of(new ParameterValidationResult(parameter, -1,
260+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null)));
261+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
262+
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
263+
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
264+
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
265+
assertThat(attributes.get("message")).asString()
266+
.isEqualTo(
267+
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
268+
assertThat(attributes).containsEntry("errors",
269+
methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
270+
}
271+
249272
@Test
250273
void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
251274
Method method = getClass().getDeclaredMethod("method", String.class);

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

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -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;
@@ -47,6 +52,7 @@
4752
* @author Phillip Webb
4853
* @author Vedran Pavic
4954
* @author Scott Frederick
55+
* @author Yanming Zhou
5056
*/
5157
class DefaultErrorAttributesTests {
5258

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

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

0 commit comments

Comments
 (0)