BindException thrown instead of MethodArgumentNotValidException in REST application - java

I have a simple Spring Rest Controller with some validation. My understanding is that validation failures would throw a MethodArgumentNotValidException. However, my code throws a BindException instead. In debug messages, I also see the app returning a null ModelAndView.
Why would a Rest Controller throw BindException or return a null ModelAndView?
Note: I am testing my web application using curl and making an HTTP POST
curl -X POST http://localhost:8080/tasks
I am intentionally omitting the "name" parameter which is a required field marked with #NotNull and #NotBlank annotations.
My Controller:
#RestController
public class TasksController {
private static Logger logger = Logger.getLogger(TasksController.class);
#Autowired
private MessageSource messageSource;
#Autowired
private Validator validator;
#InitBinder
protected void initBinder(WebDataBinder binder){
binder.setValidator(this.validator);
}
#RequestMapping(value = "/tasks", method = RequestMethod.POST)
public Task createTask(#Valid TasksCommand tasksCommand){
Task task = new Task();
task.setName(tasksCommand.getName());
task.setDue(tasksCommand.getDue());
task.setCategory(tasksCommand.getCategory());
return task;
}
}
My "command" class (that contains validation annotations)
public class TasksCommand {
#NotBlank
#NotNull
private String name;
private Calendar due;
private String category;
... getters & setters ommitted ...
}
My RestErrorHandler class:
#ControllerAdvice
public class RestErrorHandler {
private static Logger logger = Logger.getLogger(RestErrorHandler.class);
#Autowired
private MessageSource messageSource;
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ErrorsList processErrors(MethodArgumentNotValidException ex){
logger.info("error handler invoked ...");
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrorList = result.getFieldErrors();
ErrorsList errorsList = new ErrorsList();
for(FieldError fieldError: fieldErrorList){
Locale currentLocale = LocaleContextHolder.getLocale();
String errorMessage = messageSource.getMessage(fieldError, currentLocale);
logger.info("adding error message - " + errorMessage + " - to errorsList");
errorsList.addFieldError(fieldError.getField(), errorMessage);
}
return errorsList;
}
}
The processErrors method marked with #ExceptionHandler(...) annotation never gets called. If I try to catch a BindException using #ExceptionHandler(...) annotation, that handler method does get invoked.
I have couple of support classes - Task, TaskCommand, Error and ErrorsList - that I can post code for if needed.

The problem was with my curl command.
curl -d sends the Content-Type "application/x-www-form-urlencoded". As a result, Spring interprets the data as web form data (instead of JSON). Spring uses FormHttpMessageConverter to convert body of POST into domain object and results in a BindException.
What we want is for Spring to treat POST data as JSON and use the MappingJackson2HttpMessageConverter to parse body of POST into object. This can be done by specifying the "Content-Type" header with curl command:
curl -X POST -H "Content-Type: application/json" -d '{"name":"name1", "due":"2014-DEC-31 01:00:00 PDT", "category":"demo"}' http://localhost:8080/tasks
See this post for how to post JSON data using curl:
How to POST JSON data with Curl from Terminal/Commandline to Test Spring REST?
Also, here's the relevant Spring documentation about MessageConverters:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-requestbody
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html#rest-message-conversion

Related

Spring boot patch request on controller with Map parameter. Test with mockmvc

I'm trying to test a 'patch request' from my CompanyController that has a Map and Id as a parameters. I expected get a http status 200, but I get a http status 400.
Can someone explain to me what I'm doing wrong? thank you
CompanyController (some parts of the code are omitted):
#RestController
public class CompanyController {
#Autowired
private CompanyService companyService;
#PatchMapping("companies/{id}")
public ResponseEntity<CompanyDTO> patchUpdateCompany(#PathVariable Integer id,
#RequestBody Map<String, Object> updates) throws JsonMappingException {
Optional<CompanyDTO> optionalCompanyDTO = this.companyService.patchUpdateCompany(updates, id);
return ResponseEntity.ok(optionalCompanyDTO.get());
}
}
CompanyControllerTest (some parts of the code are omitted)
#WebMvcTest(CompanyController.class)
public class CompanyControllerTest {
#MockBean
private CompanyService companyService;
#Autowired
private MockMvc mockMvc;
private static List<CompanyDTO> companyDTOList;
#BeforeAll
public static void beforeAll(){
companyDTOList = new ArrayList<>();
CompanyDTO companyDTO1 = CompanyDTO.builder().id(1).name("xavi").build();
CompanyDTO companyDTO2 = CompanyDTO.builder().id(2).name("marteta").build();
companyDTOList.add(companyDTO1);
companyDTOList.add(companyDTO2);
}
#Test
void givenMapAndIdWhenPatchUpdateCompanyThenReturnHttpStatusOk() throws Exception {
Mockito.when(this.companyService.getCompanyById(1)).thenReturn(Optional.of(companyDTOList.get(0)));
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("name", "xavi2");
this.mockMvc.perform(patch("/companies/1")
.contentType(MediaType.APPLICATION_JSON)
.params(requestParams))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", Matchers.is("xavi2")));
}
}
Your problem is here
this.mockMvc.perform(patch("/companies/1")
.contentType(MediaType.APPLICATION_JSON)
.params(requestParams)) <---------------------
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", Matchers.is("xavi2")));
}
You pass the data as request parameters. But in your API you have #RequestBody meaning it expects to get the data in http request body and not as request parameters.
This is why you face 400 error meaning Bad Request which is caused by Spring having matched the URL path and also the http method type but something additional which was in the signature of API method was not provided in your request.
So you should use the .content(requestParams) method to set the content you want in the body of the request which you will send
Related documentation
Otherwise your API should have used #RequestParam instead of #RequestBody to receive the input as request parameters as previously sent from test.

Spring Boot: MockMvc returning empty body for POST request

I'm writing a small demo application with Spring Boot and Spring Data Rest. I have the following model and corresponding repository:
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String jobTitle;
public Employee() {
}
... // getters and setters
}
#RepositoryRestResource(collectionResourceRel = "employees", path = "employees")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
#RestResource(path = "by-last-name", rel = "by-last-name")
Page<Employee> findByLastNameIgnoreCase(Pageable pageable, #Param("lastName") String lastName);
#RestResource(path = "by-job-title", rel = "by-job-title")
Page<Employee> findByJobTitleIgnoreCase(Pageable pageable, #Param("jobTitle") String jobTitle);
}
If I make the following request through Postman:
POST localhost:8080/employees
{"firstName":"Test","lastName":"McTest","jobTitle":"Tester"}
I receive a full response body with my newly created entity:
{
"firstName": "Test",
"lastName": "McTest",
"jobTitle": "Tester",
"_links": {
"self": {
"href": "http://localhost:8080/employees/120"
},
"employee": {
"href": "http://localhost:8080/employees/120"
}
}
}
However, when I make the same request through my tests as shown below, I get an empty response body:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class)
#AutoConfigureMockMvc
public class EmployeeIntegrationTest {
#Autowired
private MockMvc mvc;
#Autowired
ObjectMapper objectMapper;
#Test
public void testAddEmployee() throws Exception {
Employee employee = new Employee();
employee.setFirstName("Test");
employee.setLastName("McTest");
employee.setJobTitle("Tester");
MockHttpServletRequestBuilder requestBuilder = post("/employees")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee));
mvc
.perform(requestBuilder)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName", Matchers.is("Test"))); // Fails, because content is empty.
}
}
For what it's worth, if I then perform a GET /employees in my test, I do in fact see the entity in the response body so I know it's being created.
My expectation is that I would get the same response through either method, alas that's not the case currently, and it seems as though POST requests with MockMvc aren't returning a body. Am I potentially missing a configuration setting somewhere?
Setting the HTTP Accept header in the request should cause a response body to be returned:
MockHttpServletRequestBuilder requestBuilder = post("/employees")
.accept(APPLICATION_JSON)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee));
By default Spring Data REST will only return a response body if the HTTP Accept header is specified. This behavior is documented in the Spring Data REST Reference Guide:
The POST method creates a new entity from the given request body. By default, whether the response contains a body is controlled by the Accept header sent with the request. If one is sent, a response body is created. If not, the response body is empty and a representation of the resource created can be obtained by following link contained in the Location response header. This behavior can be overridden by configuring RepositoryRestConfiguration.setReturnBodyOnCreate(…) accordingly.
My guess is that your Postman request is setting the "Accept" header, which is why you hadn't seen this behavior in Postman.
I was able to solve the issue by explicitly setting
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setReturnBodyForPutAndPost(true);
}
inside of a #Configuration class that implements RepositoryRestConfigurer
My guess would be that this is set implicitly for main code, but not for tests.

Spring Rest return a JSON response with a certain http response code

I am very new to Spring. I have a REST api written in Spring, but I don't know how to return a JSON response with a custom http response code.
I return a JSON response as follows:
public String getUser(String id){
...
return jsonObj;
}
But it always displays 200 http ok status code.
Here are my questions:
How can I synchronize the response JSON and HTTP code?
How is it possible to return JSON response and custom HTTP code in void function?
Use #ResponseStatus annotation:
#GetMapping
#ResponseStatus(HttpStatus.ACCEPTED)
public String getUser(String id) {...}
Alternative way: If you want to decide programmatically what status to return you can use ResponseEntity. Change return type of a method to ResponseEntity<String> and you'll be offered with a DSL like this:
ResponseEntity
.status(NOT_FOUND)
.contentType(TEXT_PLAIN)
.body("some body");
How I do it
Here is how I do JSON returns from a Spring Handler method.
My techniques are somewhat out-of-date,
but are still reasonable.
Configure Jackson
Add the following to the spring configuration xml file:
<bean name="jsonView"
class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
</bean>
With that,
Spring will convert return values to JSON and place them in the body of the response.
Create a utility method to build the ResponseEntity
Odds are good that you will have multiple handler methods.
Instead of boilerplate code,
create a method to do the standard work.
ResponseEntity is a Spring class.
protected ResponseEntity<ResponseJson> buildResponse(
final ResponseJson jsonResponseBody,
final HttpStatus httpStatus)
{
final ResponseEntity<ResponseJson> returnValue;
if ((jsonResponseBody != null) &&
(httpStatus != null))
{
returnValue = new ResponseEntity<>(
jsonResponseBody,
httpStatus);
}
return returnValue;
}
Annotate the handler method
#RequestMapping(value = "/webServiceUri", method = RequestMethod.POST)
you can also use the #PostMethod annotation
#PostMethod("/webServiceUri")
Return ResponseEntity from the handler method
Call the utility method to build the ResponseEntity
public ResponseEntity<ResponseJson> handlerMethod(
... params)
{
... stuff
return buildResponse(json, httpStatus);
}
Annotate the handler parameters
Jackson will convert from json to the parameter type when you use the #RequestBody annotation.
public ResponseEntity<ResponseJson> handlerMethod(
final WebRequest webRequest,
#RequestBody final InputJson inputJson)
{
... stuff
}
A different story
You can use the #JsonView annotation.
Check out the Spring Reference for details about this.
Browse to the ref page and search for #JsonView.

Testing Spring MVC POST with IntelliJ REST client causes 415

I'm developing an application in Java/Spring MVC and have no problem with testing my GET methods. The problem occur then I try to test the POST using #RequestBody.
The error:
HTTP 415 The server refused this request because the request entity is in a format not supported by the requested resource for the requested method.
I created a simple test to show my problem:
#RestController
#RequestMapping("/test")
public class ConcreteTestController implements TestController {
#RequestMapping(method = RequestMethod.POST)
#ResponseStatus(value = HttpStatus.OK)
#Override
public void add(#RequestBody Dummy dummy) {
System.out.println(dummy);
}
#RequestMapping(method = RequestMethod.GET)
#ResponseStatus(value = HttpStatus.OK)
#Override
public Dummy get() {
Dummy dummy = new Dummy();
dummy.setName("apa");
return dummy;
}
}
The Dummy class is very simple:
public class Dummy {
private String name;
public Dummy() {}
// Omitted setters and getters.
}
The jsonresponse from the GET looks like this:
{"name":"apa"}
I'm starting the IntelliJ REST client and using the json above as request body. I've tried using both application/json and / under Accept in the header with no difference in result.
Any idea what could cause this? I'm stuck and would appreciate help.
By default you have to add Content-Type manually in the REST client in IntelliJ. I had forgotten to do so and to set it to application/json. After having done so it is working fine.

global binding initializer issues when multiple parameters with the same type exists in the form

I have the following setup:
#ControllerAdvice
public class GlobalBindingInitializer {
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(ZonedDateTime.class, new ZonedDateTimeBinder());
}
}
public class ZonedDateTimeBinder extends PropertyEditorSupport {
public void setAsText(String value) {
setValue(ZonedDateTime.parse(value));
}
public String getAsText() {
return ((ZonedDateTime) getValue()).toString();
}
}
#RequestMapping(value = "/bx", method = RequestMethod.POST)
public ResponseEntity<X> bx(#Valid #RequestBody MyForm form, BindingResult result) {
.............
}
public class AddNewBookingPeriodForm {
.....
#NotNull
private ZonedDateTime dateFrom;
#NotNull
private ZonedDateTime dateTo;
.....
}
#Test
public void test_bx_valid() throws Exception {
String content = "{" +
"\"uuid\":\""+...+"\"," +
"\"dateFrom\":\""+...+"\"," +
"\"dateTo\":\""+...+"\"," +
"\"ln\":\""+...+"\"," +
"\"fn\":\""+...+"\"," +
"\"p\":\""+...+"\"" +
"}";
mockMvc.perform(post("/b/bx")
.contentType("application/json;charset=UTF-8")
.content((content).getBytes()))
.andDo(print())
.andExpect(status().isCreated())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect((jsonPath("$.", Matchers.hasSize(3))))
.andDo(print());
}
The problem is that when I run the test, it finds the controller handler method and says the it cannot read the request because of the body. I debugged the sent message and it has the required form, it's a valid JSON object. The problem is with the two ZonedDateTime fields.
I have a similar code in another place but the form used for validation has only one ZonedDateTime and it works. I think the problem may be because I have two in this form but I can't figure out what is happening.
I tried changing the type of dateFrom and dateTo from my form to String and everything worked just fine, so the problem is with those two.
Any ideas?
This debug message printed by the test:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /b/bx
Parameters = {}
Headers = {Content-Type=[application/json;charset=UTF-8]}
Handler:
Type = valid handler controller
Method = valid handler method
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotReadableException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = null
Headers = {}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
EDIT 1
Here is a valid sample
{
"uuid":"e50b5cbf-c81a-40de-9eee-ceecd21ad179",
"dateFrom":"2015-09-07T19:25:42+03:00",
"dateTo":"2015-09-08T19:25:42+03:00",
"ln":"ln",
"fn":"fn",
"p":"007"
}
I think the problem may be coming from #RequestBody annotation because in the working case I talked about in the previous rows I use the GET method without the #RequestBody annotation and the mapping works just fine.
Given RequestBody, Spring will use an HttpMessageConverter to deserialize your request body into an instance of your given type. In this case, it will use MappingJackson2HttpMessageConverter for the JSON. This converter does not involve your PropertyEditorSupport at all.
To deserialize ZonedDateTime correctly, you'll to register an appropriate Jackson module for mapping java.time types. One possibility is jackson-datatype-jsr310. I believe it should be registered automatically when found on the classpath. If it's not, you'll need to register it manually by creating and registering a MappingJackson2HttpMessageConverter manually with an appropriately created ObjectMapper.

Categories

Resources