Skip to content

Commit 6b6f1c8

Browse files
committed
Include errors from HandlerMethodValidationException for DefaultErrorAttributes
Fix GH-39858
1 parent 8b4f411 commit 6b6f1c8

File tree

4 files changed

+129
-7
lines changed

4 files changed

+129
-7
lines changed

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

+11
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;
@@ -58,6 +59,7 @@
5859
* @author Michele Mancioppi
5960
* @author Scott Frederick
6061
* @author Moritz Halbritter
62+
* @author Yanming Zhou
6163
* @since 2.0.0
6264
* @see ErrorAttributes
6365
*/
@@ -117,6 +119,10 @@ private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus
117119
if (error instanceof BindingResult) {
118120
return error.getMessage();
119121
}
122+
if (error instanceof MethodValidationResult methodValidationResult) {
123+
return "Validation failed for method: %s, with %d error(s)".formatted(methodValidationResult.getMethod(),
124+
methodValidationResult.getAllErrors().size());
125+
}
120126
if (error instanceof ResponseStatusException responseStatusException) {
121127
return responseStatusException.getReason();
122128
}
@@ -151,6 +157,11 @@ private void handleException(Map<String, Object> errorAttributes, Throwable erro
151157
errorAttributes.put("errors", result.getAllErrors());
152158
}
153159
}
160+
if (error instanceof MethodValidationResult result) {
161+
if (result.hasErrors()) {
162+
errorAttributes.put("errors", result.getAllErrors());
163+
}
164+
}
154165
}
155166

156167
@Override

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

+27-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.util.StringUtils;
3737
import org.springframework.validation.BindingResult;
3838
import org.springframework.validation.ObjectError;
39+
import org.springframework.validation.method.MethodValidationResult;
3940
import org.springframework.web.context.request.RequestAttributes;
4041
import org.springframework.web.context.request.WebRequest;
4142
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -62,6 +63,7 @@
6263
* @author Vedran Pavic
6364
* @author Scott Frederick
6465
* @author Moritz Halbritter
66+
* @author Yanming Zhou
6567
* @since 2.0.0
6668
* @see ErrorAttributes
6769
*/
@@ -149,13 +151,20 @@ private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest web
149151
}
150152

151153
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);
154+
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
155+
if (methodValidationResult != null) {
156+
addMethodValidationResultErrorMessage(errorAttributes, methodValidationResult);
155157
}
156158
else {
157-
addBindingResultErrorMessage(errorAttributes, result);
159+
BindingResult bindingResult = extractBindingResult(error);
160+
if (bindingResult != null) {
161+
addBindingResultErrorMessage(errorAttributes, bindingResult);
162+
}
163+
else {
164+
addExceptionErrorMessage(errorAttributes, webRequest, error);
165+
}
158166
}
167+
159168
}
160169

161170
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
@@ -193,13 +202,27 @@ private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, B
193202
errorAttributes.put("errors", result.getAllErrors());
194203
}
195204

205+
private void addMethodValidationResultErrorMessage(Map<String, Object> errorAttributes,
206+
MethodValidationResult result) {
207+
errorAttributes.put("message", "Validation failed for method='" + result.getMethod() + "'. " + "Error count: "
208+
+ result.getAllErrors().size());
209+
errorAttributes.put("errors", result.getAllErrors());
210+
}
211+
196212
private BindingResult extractBindingResult(Throwable error) {
197213
if (error instanceof BindingResult bindingResult) {
198214
return bindingResult;
199215
}
200216
return null;
201217
}
202218

219+
private MethodValidationResult extractMethodValidationResult(Throwable error) {
220+
if (error instanceof MethodValidationResult methodValidationResult) {
221+
return methodValidationResult;
222+
}
223+
return null;
224+
}
225+
203226
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
204227
StringWriter stackTrace = new StringWriter();
205228
error.printStackTrace(new PrintWriter(stackTrace));

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

+42
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,44 @@ 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 = new MethodValidationResult() {
284+
285+
@Override
286+
public Object getTarget() {
287+
return target;
288+
}
289+
290+
@Override
291+
public Method getMethod() {
292+
return method;
293+
}
294+
295+
@Override
296+
public boolean isForReturnValue() {
297+
return false;
298+
}
299+
300+
@Override
301+
public List<ParameterValidationResult> getAllValidationResults() {
302+
return List.of(new ParameterValidationResult(parameter, -1,
303+
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null));
304+
}
305+
};
306+
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
307+
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
308+
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
309+
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
310+
assertThat(attributes.get("message")).asString()
311+
.isEqualTo(
312+
"Validation failed for method: public java.lang.String java.lang.String.substring(int), with 1 error(s)");
313+
assertThat(attributes).containsEntry("errors", methodValidationResult.getAllErrors());
314+
}
315+
274316
@Test
275317
void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception {
276318
Method method = getClass().getDeclaredMethod("method", String.class);

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

+49-3
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;
@@ -48,6 +54,7 @@
4854
* @author Vedran Pavic
4955
* @author Scott Frederick
5056
* @author Moritz Halbritter
57+
* @author Yanming Zhou
5158
*/
5259
class DefaultErrorAttributesTests {
5360

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

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

0 commit comments

Comments
 (0)