BindException thrown instead of MethodArgumentNotValidException in REST application
Asked Answered
U

2

12

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.

Unvarnished answered 22/4, 2014 at 5:27 Comment(2)
I have the exact same problem in my @RestController. It works when I use @Controller combined with @RequestBody and @ResponseBody. Did you work this out by any chance?Bereniceberenson
I just responded with my answer. Please see below. Although your issue might be something different. It should work either way.Unvarnished
U
7

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

Unvarnished answered 12/11, 2014 at 20:0 Comment(0)
C
0

Although this question is nearly 10 years old (at time of writing), I want to add something to it, for the benefit of anyone else who, like me, found this question by exploring "related questions" that were linked to other, SO questions.

First of all, I don't think that the OP's problem can be solved by only changing request content type.

Spring automatically infers a suitable annotation for your request handler (i.e. the Controller) method according to "Any other argument" row in the table from here:

https://docs.spring.io/spring-framework/reference/6.1.5/web/webmvc/mvc-controller/ann-methods/arguments.html

Which says

If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by BeanUtils#isSimpleProperty), it is resolved as a @RequestParam. Otherwise, it is resolved as a @ModelAttribute.

So the processed signature should look like this:

@RequestMapping(value = "/tasks", method = RequestMethod.POST)
public Task createTask(@Valid @ModelAttribute TasksCommand tasksCommand)

And to my understanding, @ModelAttribute is for key-value paired strings (e.g. form-url-encoded, the actual body structure is something like key1=value1&key2=value2 which are key-value pairs concated with symbol & and then got "url-encoded"), while @RequestBody is the one for JSON body.

As I just tested, only changing the content type and the input format to JSON, will only result in params annotated with @ModelAttribute being empty.

@ModelAttribute seems to be the only way you can get a BindException when validation failed. @RequestBody and @RequestParam annotated parameters will throw MethodArgumentNotValidException.

What I think under the hood: When the ModelAttributeMethodProcessor was doing data binding in constructAttribute() it found out a validation failed, then it wrapped the Exception using BindException (spring-web-5.2.15.RELEASE ModelAttributeMethodProcessor.java:329), presumably after MessageConverter's job was done. (I don't know more specifically as I was studying data binding when I stumbled upon this post.)

Cockboat answered 20/3 at 8:53 Comment(1)
ModelAttributeMethodProcessor throws a MethodArgumentNotValidException since version 6.0.Spirochete

© 2022 - 2024 — McMap. All rights reserved.