Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

multipart/form-data binding for ModelAttribute fails if multipart file param is null

I have a controller mapped for processing uploaded files

Controller

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(
      final @RequestParam("g-recaptcha-response") String captchaResponse,
      final @RequestParam("file") MultipartFile file,
      final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
      final Model model)  

form

<form name="jobsForm" id="jobsForm" novalidate="novalidate" action="#" th:action="@{/careers/pursue}"
                    th:object="${jobapplication}" method="post" enctype="multipart/form-data">
    <div class="control-group form-group">
        <div class="controls">
            <label>First Name:</label>
            <input type="text" class="form-control" id="firstName" th:field="*{firstName}" required="required" data-validation-required-message="Please enter your name." />
            <p class="help-block"></p>
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Last Name:</label>
            <input type="text" class="form-control" id="lastName" th:field="*{lastName}" required="required" data-validation-required-message="Please enter your name." />
            <p class="help-block"></p>
        </div>
    </div>                    
    <div class="control-group form-group">
        <div class="controls">
            <label>Phone Number:</label>
            <input type="tel" class="form-control" id="phone" th:field="*{phone}" required="required" data-validation-required-message="Please enter your phone number." />
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Email Address:</label>
            <input type="email" class="form-control" id="email" th:field="*{email}" required="required" data-validation-required-message="Please enter your email address."/>
        </div>
    </div>
    <div class="control-group form-group">
        <div class="controls">
            <label>Role:</label>
            <input type="email" class="form-control" id="role" th:field="*{role}" required="required" data-validation-required-message="Please enter your email address."/>
        </div>
    </div>                    
    <div class=" control-group form-group">
                            <div class="g-recaptcha" data-sitekey="ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"></div>                    
    </div>
    <div class=" control-group form-group">
         <span class="btn btn-primary btn-file">
                                Add your Resumé <input type="file" name="file" id="file" required="required"/>
                         </span>
    </div>
    <div id="success"></div>
    <!-- For success/fail messages -->
    <button type="submit" class="btn btn-primary">Apply!</button>
</form>

Now, if a person misses attaching a file to the form before submit,

-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="firstName"

Anadi
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="lastName"

Misra
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="phone"

9845420420
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="email"

[email protected]
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="role"

open.project
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="g-recaptcha-response"

03AHJ_Vuv9i7WQ_4zCipfnyrLNl6467l_cZgGIhkdpLjS1M0YmWvwQMOWQeRcrAHFh8s3-jO13NQs7019lzI7UobwNeHKIhBmcLMiVGPk38Iy8BjrEi2glI4QGjE4VTvRhV_-WWYsmlzV_7PRPE5Y8L0NboPXYoG9JSabMOL8V958w74pOzkxabsoR4wouCSa0gzo0EbOsLiCWjd0MAvZiCcKJGdwIlMp0WIjxcufB-RfG2F0rwv65yrgL-By0bdMewkWULY_aRvC-FRSqOEM9X5Qg4gviA-cvc5IY2XnRtaUALOPlR_QbwjgUKl2mJEFNab6Pks3MlsivuEZFkba4isDFlrJ4jXwBBQ
-----------------------------749526091303082321866336941
Content-Disposition: form-data; name="file"; filename=""
Content-Type: application/octet-stream


-----------------------------749526091303082321866336941--

or tries to submit without validating captcha, I get this exception

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring4.processor.attr.SpringInputGeneralFieldAttrProcessor' (jobs:91)
....
....
Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'jobapplication' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:144)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatusFromParsedExpression(FieldUtils.java:396)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:323)
at org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:289)
at org.thymeleaf.spring4.processor.attr.AbstractSpringFieldAttrProcessor.processAttribute(AbstractSpringFieldAttrProcessor.java:98)
at org.thymeleaf.processor.attr.AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)
at org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)
... 66 common frames omitted

What I expect is that I get empty values for captcha response and file and then my controller method should be able handle it, and send user back to the form with specific error message. It works like so on forms without multipart data, i.e. I do not get binding errors but null values in the controller arguments. I see this issue only when I use Multipart Form data, the binding goes all fine if all the data is populated, i.e. a user verifies captcha, and attaches a file.

Making these params optional or using RequestPart hasn't also helped (I admit I really do not get what is the purpose of RequestPartannotation) So, changing the controller to this (knee jerk experimentations ;-))

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(
      final @RequestPart(value = "g-recaptcha-response", required = false) String captchaResponse,
      final @RequestPart(value = "file", required = false) MultipartFile file,
      final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
      final Model model) 

does not help either. Do I have to extend StandardServletMultipartResolver or is it something to change/fix in SpringInputGeneralFieldAttrProcessor, or am I missing some minor detail here?

Update

Adding Controller method

  @RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(final @ModelAttribute("jobapplication") @Valid JobApplication application,
      final BindingResult bindingResult, final Model model,
      final @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
      final @RequestPart(value = "file", required = false) MultipartFile file) {
    return new Callable<String>() {
      @Override
      public String call() throws Exception {
        try {
          model.asMap().clear();
          GoogleCaptchaResponseData response = captchaVerifier.isCaptchaResponseValid(captchaResponse).get();
          model.addAttribute("recaptcha", response.isSuccess());
          model.addAttribute("recaptchamessage", response.getErrorCodes());

          if (response.isSuccess() && !file.isEmpty()) {
            byte[] bytes = file.getBytes();

            LOGGER.info("Found file of type {}", file.getOriginalFilename());
            ByteArrayInputStream inputBytes = new ByteArrayInputStream(bytes);
            mailApi.sendMail(mailApi.buildJobApplicationEmail(application, new BufferedInputStream(inputBytes)));
            model.asMap().clear();
            model.addAttribute("uploadsuccess", true);
            model.addAttribute("resource_host", resourceHost);
            model.addAttribute("jobapplication", new JobApplication());
          }
        } catch (InterruptedException | ExecutionException e) {
          LOGGER.error(e.getMessage(), e);
          model.asMap().clear();
          model.addAttribute("jobapplication", application);
          model.addAttribute("resource_host", resourceHost);
          model.addAttribute("uploadsuccess", false);
        }
        return "jobs";
      }
    };
  }
like image 853
Anadi Misra Avatar asked Feb 27 '15 04:02

Anadi Misra


2 Answers

@RequestPart relies on HttpMessageConvertors and content-type to bind the multipart data to method param, whereas @RequestParam relies on registered convertors to do the conversion. Spring mvc provides certain convertors by default. You can use either @RequestParam or @RequestPart to bind the file data. Most of the applications use commons file upload for uploading file and register

org.springframework.web.multipart.commons.CommonsMultipartResolver

for multi part resolving. When this is registered, spring check every request for multi part data and use it to resolve this to method arg. check here

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-multipart

There are couple of items you can try. Make sure both of your captcha and file params are optional like below in your controller. I switched to @RequestParam for captcha.

@RequestMapping(value = "/careers/pursue", method = RequestMethod.POST)
  public Callable<String> pursue(
      final @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
      final @RequestPart(value = "file", required = false) MultipartFile file,
      final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
      final Model model) 

Hope this helps.

like image 88
minion Avatar answered Nov 15 '22 11:11

minion


  1. Your Controller @RequestMapping isn't mapped to the same path as for form
  2. In your form make sure your input name for the file and captcha match the @RequestParam
  3. @RequestParam doesn't pass a null notice I'm looking for length() == 0 and file.isEmpty()

Also you may want to take a look at the spring guide for file upload

Controller

@RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String pursue(
            final @RequestParam("g-recaptcha-response") String captchaResponse,
            final @RequestParam("file") MultipartFile file,
            final @ModelAttribute("jobapplication") @Valid JobApplication application, final BindingResult bindingResult,
            final Model model)
    {
        if (bindingResult.hasErrors() || captchaResponse.length() == 0 || file.isEmpty())
        {
            return "form";
        }

        return "redirect:/";
    }

Form

<form name="jobsForm" id="jobsForm" novalidate="novalidate" action="#" th:action="@{/upload}"
      th:object="${jobapplication}" method="post" enctype="multipart/form-data">
    <span>First Name: </span>
    <input id="firstname" th:field="*{firstName}" type="text"/><br/>
    <span>Last Name: </span>
    <input id="lastname" th:field="*{lastName}" type="text"/><br/>
    <span>Phone: </span>
    <input id="phone" th:field="*{phone}" type="text"/><br/>
    <span>Email: </span>
    <input id="email" th:field="*{email}" type="text"/><br/>
    <span>Role: </span>
    <input id="role" th:field="*{role}" type="text"/><br/>
    <span></span>
    <input type="text" name="g-recaptcha-response"/><br/>
    <span>File: </span>
    <input type="file" name="file"/><br/>
    <input id="submit" type="submit"/>
</form>
like image 24
ndrone Avatar answered Nov 15 '22 10:11

ndrone