From 266335339db46d2487ffafcff9a8c0a316552abc Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 14 Oct 2015 13:28:42 +0200 Subject: [PATCH] Extract BindingResult if necessary Previously, no `errors` attribute is made available in the standard JSON error document if a request body object is invalid. This is due to the fact that the framework throws a `MethodArgumentNotValidException holding a `BindingResult` object that was not detected. We now make sure to extract the `BindingResult` from such exception. Closes gh-4166 --- .../web/DefaultErrorAttributes.java | 18 +++++++-- .../BasicErrorControllerIntegrationTests.java | 37 +++++++++++++++++++ .../web/DefaultErrorAttributesTests.java | 18 ++++++++- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributes.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributes.java index a291bff3807..05a0d08632a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributes.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; @@ -52,6 +53,7 @@ import org.springframework.web.servlet.ModelAndView; * * @author Phillip Webb * @author Dave Syer + * @author Stephane Nicoll * @since 1.1.0 * @see ErrorAttributes */ @@ -130,11 +132,11 @@ public class DefaultErrorAttributes } private void addErrorMessage(Map errorAttributes, Throwable error) { - if (!(error instanceof BindingResult)) { + BindingResult result = extractBindingResult(error); + if (result == null) { errorAttributes.put("message", error.getMessage()); return; } - BindingResult result = (BindingResult) error; if (result.getErrorCount() > 0) { errorAttributes.put("errors", result.getAllErrors()); errorAttributes.put("message", @@ -146,6 +148,16 @@ public class DefaultErrorAttributes } } + private BindingResult extractBindingResult(Throwable error) { + if (error instanceof BindingResult) { + return (BindingResult) error; + } + if (error instanceof MethodArgumentNotValidException) { + return ((MethodArgumentNotValidException) error).getBindingResult(); + } + return null; + } + private void addStackTrace(Map errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java index 0359603ee01..0cb355a098f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerIntegrationTests.java @@ -24,6 +24,8 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; import org.junit.After; import org.junit.Test; @@ -45,7 +47,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.validation.BindException; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.View; @@ -164,6 +168,20 @@ public class BasicErrorControllerIntegrationTests { assertThat(resp, containsString("org.springframework.validation.BindException")); } + @Test + @SuppressWarnings("rawtypes") + public void testRequestBodyValidationForMachineClient() throws Exception { + load(); + RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation"))) + .contentType(MediaType.APPLICATION_JSON).body("{}"); + ResponseEntity entity = new TestRestTemplate().exchange(request, Map.class); + String resp = entity.getBody().toString(); + assertThat(resp, containsString("Error count: 1")); + assertThat(resp, containsString("errors=[{")); + assertThat(resp, containsString("codes=[")); + assertThat(resp, containsString("org.springframework.web.bind.MethodArgumentNotValidException")); + } + private void assertErrorAttributes(Map content, String status, String error, Class exception, String message, String path) { assertEquals("Wrong status", status, content.get("status")); @@ -239,6 +257,11 @@ public class BasicErrorControllerIntegrationTests { throw error; } + @RequestMapping(path = "/bodyValidation", method = RequestMethod.POST, produces = "application/json") + public String bodyValidation(@Valid @RequestBody DummyBody body) { + return body.content; + } + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!") @SuppressWarnings("serial") private static class ExpectedException extends RuntimeException { @@ -255,6 +278,20 @@ public class BasicErrorControllerIntegrationTests { } + private static class DummyBody { + + @NotNull + private String content; + + public String getContent() { + return this.content; + } + + public void setContent(String content) { + this.content = content; + } + } + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java index 407596fac8b..23b25269a9a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.MapBindingResult; import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.ModelAndView; @@ -162,7 +163,20 @@ public class DefaultErrorAttributesTests { BindingResult bindingResult = new MapBindingResult( Collections.singletonMap("a", "b"), "objectName"); bindingResult.addError(new ObjectError("c", "d")); - BindException ex = new BindException(bindingResult); + Exception ex = new BindException(bindingResult); + testBindingResult(bindingResult, ex); + } + + @Test + public void extractMethodArgumentNotValidExceptionBindingResultErrors() throws Exception { + BindingResult bindingResult = new MapBindingResult( + Collections.singletonMap("a", "b"), "objectName"); + bindingResult.addError(new ObjectError("c", "d")); + Exception ex = new MethodArgumentNotValidException(null, bindingResult); + testBindingResult(bindingResult, ex); + } + + private void testBindingResult(BindingResult bindingResult, Exception ex) { this.request.setAttribute("javax.servlet.error.exception", ex); Map attributes = this.errorAttributes .getErrorAttributes(this.requestAttributes, false);