Skip to content

Commit 20e9ff9

Browse files
quaffwilkinsona
authored andcommitted
Consider HandlerMethodValidationException in DefaultErrorAttributes
See gh-39865
1 parent 4b61ae4 commit 20e9ff9

File tree

4 files changed

+141
-7
lines changed

4 files changed

+141
-7
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.util.StringUtils;
3333
import org.springframework.validation.BindingResult;
3434
import org.springframework.validation.ObjectError;
35+
import org.springframework.validation.method.MethodValidationResult;
3536
import org.springframework.web.bind.annotation.ResponseStatus;
3637
import org.springframework.web.reactive.function.server.ServerRequest;
3738
import org.springframework.web.server.ResponseStatusException;
@@ -57,6 +58,7 @@
5758
* @author Stephane Nicoll
5859
* @author Michele Mancioppi
5960
* @author Scott Frederick
61+
* @author Yanming Zhou
6062
* @since 2.0.0
6163
* @see ErrorAttributes
6264
*/
@@ -113,6 +115,14 @@ private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus
113115
if (error instanceof BindingResult) {
114116
return error.getMessage();
115117
}
118+
if (error instanceof MethodValidationResult methodValidationResult) {
119+
long errorCount = methodValidationResult.getAllErrors()
120+
.stream()
121+
.filter(ObjectError.class::isInstance)
122+
.count();
123+
return "Validation failed for method: %s, with %d %s".formatted(methodValidationResult.getMethod(),
124+
errorCount, (errorCount > 1) ? "errors" : "error");
125+
}
116126
if (error instanceof ResponseStatusException responseStatusException) {
117127
return responseStatusException.getReason();
118128
}
@@ -147,6 +157,12 @@ private void handleException(Map<String, Object> errorAttributes, Throwable erro
147157
errorAttributes.put("errors", result.getAllErrors());
148158
}
149159
}
160+
if (error instanceof MethodValidationResult result) {
161+
if (result.hasErrors()) {
162+
errorAttributes.put("errors",
163+
result.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
164+
}
165+
}
150166
}
151167

152168
@Override

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

Lines changed: 33 additions & 4 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;
@@ -29,13 +30,15 @@
2930

3031
import org.springframework.boot.web.error.ErrorAttributeOptions;
3132
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
33+
import org.springframework.context.MessageSourceResolvable;
3234
import org.springframework.core.Ordered;
3335
import org.springframework.core.annotation.Order;
3436
import org.springframework.http.HttpStatus;
3537
import org.springframework.util.ObjectUtils;
3638
import org.springframework.util.StringUtils;
3739
import org.springframework.validation.BindingResult;
3840
import org.springframework.validation.ObjectError;
41+
import org.springframework.validation.method.MethodValidationResult;
3942
import org.springframework.web.context.request.RequestAttributes;
4043
import org.springframework.web.context.request.WebRequest;
4144
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -61,6 +64,7 @@
6164
* @author Stephane Nicoll
6265
* @author Vedran Pavic
6366
* @author Scott Frederick
67+
* @author Yanming Zhou
6468
* @since 2.0.0
6569
* @see ErrorAttributes
6670
*/
@@ -145,13 +149,20 @@ private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest web
145149
}
146150

147151
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);
152+
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
153+
if (methodValidationResult != null) {
154+
addMethodValidationResultErrorMessage(errorAttributes, methodValidationResult);
151155
}
152156
else {
153-
addBindingResultErrorMessage(errorAttributes, result);
157+
BindingResult bindingResult = extractBindingResult(error);
158+
if (bindingResult != null) {
159+
addBindingResultErrorMessage(errorAttributes, bindingResult);
160+
}
161+
else {
162+
addExceptionErrorMessage(errorAttributes, webRequest, error);
163+
}
154164
}
165+
155166
}
156167

157168
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
@@ -189,13 +200,31 @@ private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, B
189200
errorAttributes.put("errors", result.getAllErrors());
190201
}
191202

203+
private void addMethodValidationResultErrorMessage(Map<String, Object> errorAttributes,
204+
MethodValidationResult result) {
205+
List<? extends MessageSourceResolvable> errors = result.getAllErrors()
206+
.stream()
207+
.filter(ObjectError.class::isInstance)
208+
.toList();
209+
errorAttributes.put("message",
210+
"Validation failed for method='" + result.getMethod() + "'. " + "Error count: " + errors.size());
211+
errorAttributes.put("errors", errors);
212+
}
213+
192214
private BindingResult extractBindingResult(Throwable error) {
193215
if (error instanceof BindingResult bindingResult) {
194216
return bindingResult;
195217
}
196218
return null;
197219
}
198220

221+
private MethodValidationResult extractMethodValidationResult(Throwable error) {
222+
if (error instanceof MethodValidationResult methodValidationResult) {
223+
return methodValidationResult;
224+
}
225+
return null;
226+
}
227+
199228
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
200229
StringWriter stackTrace = new StringWriter();
201230
error.printStackTrace(new PrintWriter(stackTrace));

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

Lines changed: 43 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;
@@ -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,45 @@ 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 = new MethodValidationResult() {
259+
260+
@Override
261+
public Object getTarget() {
262+
return target;
263+
}
264+
265+
@Override
266+
public Method getMethod() {
267+
return method;
268+
}
269+
270+
@Override
271+
public boolean isForReturnValue() {
272+
return false;
273+
}
274+
275+
@Override
276+
public List<ParameterValidationResult> getAllValidationResults() {
277+
return List.of(new ParameterValidationResult(parameter, -1,
278+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null));
279+
}
280+
};
281+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
282+
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
283+
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
284+
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
285+
assertThat(attributes.get("message")).asString()
286+
.isEqualTo(
287+
"Validation failed for method: public java.lang.String java.lang.String.substring(int), with 1 error");
288+
assertThat(attributes).containsEntry("errors",
289+
methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
290+
}
291+
249292
@Test
250293
void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
251294
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: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
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;
24+
import java.util.function.Supplier;
2325

2426
import jakarta.servlet.ServletException;
2527
import org.junit.jupiter.api.Test;
2628

2729
import org.springframework.boot.web.error.ErrorAttributeOptions;
2830
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
31+
import org.springframework.context.MessageSourceResolvable;
2932
import org.springframework.core.MethodParameter;
3033
import org.springframework.http.HttpStatus;
3134
import org.springframework.mock.web.MockHttpServletRequest;
@@ -34,9 +37,12 @@
3437
import org.springframework.validation.BindingResult;
3538
import org.springframework.validation.MapBindingResult;
3639
import org.springframework.validation.ObjectError;
40+
import org.springframework.validation.method.MethodValidationResult;
41+
import org.springframework.validation.method.ParameterValidationResult;
3742
import org.springframework.web.bind.MethodArgumentNotValidException;
3843
import org.springframework.web.context.request.ServletWebRequest;
3944
import org.springframework.web.context.request.WebRequest;
45+
import org.springframework.web.method.annotation.HandlerMethodValidationException;
4046
import org.springframework.web.servlet.ModelAndView;
4147

4248
import static org.assertj.core.api.Assertions.assertThat;
@@ -47,6 +53,7 @@
4753
* @author Phillip Webb
4854
* @author Vedran Pavic
4955
* @author Scott Frederick
56+
* @author Yanming Zhou
5057
*/
5158
class DefaultErrorAttributesTests {
5259

@@ -201,18 +208,57 @@ void withMethodArgumentNotValidExceptionBindingErrors() {
201208
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
202209
}
203210

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 = new MethodValidationResult() {
217+
218+
@Override
219+
public Object getTarget() {
220+
return target;
221+
}
222+
223+
@Override
224+
public Method getMethod() {
225+
return method;
226+
}
227+
228+
@Override
229+
public boolean isForReturnValue() {
230+
return false;
231+
}
232+
233+
@Override
234+
public List<ParameterValidationResult> getAllValidationResults() {
235+
return List.of(new ParameterValidationResult(parameter, -1,
236+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null));
237+
}
238+
};
239+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
240+
testErrorsSupplier(methodValidationResult::getAllErrors,
241+
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1",
242+
ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
243+
}
244+
204245
private void testBindingResult(BindingResult bindingResult, Exception ex, ErrorAttributeOptions options) {
246+
testErrorsSupplier(bindingResult::getAllErrors, "Validation failed for object='objectName'. Error count: 1", ex,
247+
options);
248+
}
249+
250+
private void testErrorsSupplier(Supplier<List<? extends MessageSourceResolvable>> errorsSupplier,
251+
String expectedMessage, Exception ex, ErrorAttributeOptions options) {
205252
this.request.setAttribute("jakarta.servlet.error.exception", ex);
206253
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest, options);
207254
if (options.isIncluded(Include.MESSAGE)) {
208-
assertThat(attributes).containsEntry("message",
209-
"Validation failed for object='objectName'. Error count: 1");
255+
assertThat(attributes).containsEntry("message", expectedMessage);
210256
}
211257
else {
212258
assertThat(attributes).doesNotContainKey("message");
213259
}
214260
if (options.isIncluded(Include.BINDING_ERRORS)) {
215-
assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors());
261+
assertThat(attributes).containsEntry("errors", errorsSupplier.get());
216262
}
217263
else {
218264
assertThat(attributes).doesNotContainKey("errors");

0 commit comments

Comments
 (0)