How to send Multipart form data with restTemplate Spring-mvc

96,531

Solution 1

Reading the whole file in a ByteArrayResource can be a memory consumption issue with large files.

You can proxy a file upload in a spring mvc controller using a InputStreamResource:

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public ResponseEntity<?> uploadImages(@RequestPart("images") final MultipartFile[] files) throws IOException {
    LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
    String response;
    HttpStatus httpStatus = HttpStatus.CREATED;

    try {
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                map.add("images", new MultipartInputStreamFileResource(file.getInputStream(), file.getOriginalFilename()));
            }
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        String url = "http://example.com/upload";

        HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers);
        response = restTemplate.postForObject(url, requestEntity, String.class);

    } catch (HttpStatusCodeException e) {
        httpStatus = HttpStatus.valueOf(e.getStatusCode().value());
        response = e.getResponseBodyAsString();
    } catch (Exception e) {
        httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        response = e.getMessage();
    }

    return new ResponseEntity<>(response, httpStatus);
}

class MultipartInputStreamFileResource extends InputStreamResource {

    private final String filename;

    MultipartInputStreamFileResource(InputStream inputStream, String filename) {
        super(inputStream);
        this.filename = filename;
    }

    @Override
    public String getFilename() {
        return this.filename;
    }

    @Override
    public long contentLength() throws IOException {
        return -1; // we do not want to generally read the whole stream into memory ...
    }
}

Solution 2

You are getting the exception because none of RestTemplate's default MessageConverters know how to serialize the InputStream contained by the MultipartFile file. When sending objects via RestTemplate, in most cases you want to send POJOs. You can fix this by adding the bytes of the MultipartFile to the MultiValueMap instead of the MultipartFile itself.

I think there is also something wrong with your servlet part. For instance

File file1 = (File) req.getAttribute("userfile1");

should always return null, as ServletRequest's getAttribute method does not return request/form parameters but attributes set by the servlet context. Are you sure it is actually working with your curl example?

Here is an example of a Spring MVC method forwarding a file to a servlet:

Servlet (though I tested it running in a Spring MVC container), adapted from here:

@RequestMapping("/pi")
private void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

  final String path = request.getParameter("destination");
  final Part filePart = request.getPart("file");
  final String fileName = request.getParameter("filename");

  OutputStream out = null;
  InputStream fileContent = null;
  final PrintWriter writer = response.getWriter();

  try {
    out = new FileOutputStream(new File(path + File.separator
            + fileName));
    fileContent = filePart.getInputStream();

    int read = 0;
    final byte[] bytes = new byte[1024];

    while ((read = fileContent.read(bytes)) != -1) {
      out.write(bytes, 0, read);
    }
    writer.println("New file " + fileName + " created at " + path);

  } catch (FileNotFoundException fne) {
    writer.println("You either did not specify a file to upload or are "
            + "trying to upload a file to a protected or nonexistent "
            + "location.");
    writer.println("<br/> ERROR: " + fne.getMessage());

  } finally {
    if (out != null) {
      out.close();
    }
    if (fileContent != null) {
      fileContent.close();
    }
    if (writer != null) {
      writer.close();
    }
  }
}

Spring MVC method:

@ResponseBody
@RequestMapping(value="/upload/", method=RequestMethod.POST, 
        produces = "text/plain")
public String uploadFile(MultipartHttpServletRequest request) 
        throws IOException {

  Iterator<String> itr = request.getFileNames();

  MultipartFile file = request.getFile(itr.next());
  MultiValueMap<String, Object> parts = 
          new LinkedMultiValueMap<String, Object>();
  parts.add("file", new ByteArrayResource(file.getBytes()));
  parts.add("filename", file.getOriginalFilename());

  RestTemplate restTemplate = new RestTemplate();
  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(MediaType.MULTIPART_FORM_DATA);

  HttpEntity<MultiValueMap<String, Object>> requestEntity =
          new HttpEntity<MultiValueMap<String, Object>>(parts, headers);

  // file upload path on destination server
  parts.add("destination", "./");

  ResponseEntity<String> response =
          restTemplate.exchange("http://localhost:8080/pi", 
                  HttpMethod.POST, requestEntity, String.class);

  if (response != null && !response.getBody().trim().equals("")) {
    return response.getBody();
  }

  return "error";
}

Using these I can succesfully upload a file through the MVC method to the servlet by the following curl:

curl --form [email protected] localhost:8080/upload/

Solution 3

Since version 5.1 the Spring Framework ships with its own Resource implementation for MultipartFiles. You can therefore simplify Lorenzo's answer by removing the MultipartInputStreamFileResource class and filling the map as follows:

[...]

for (MultipartFile file : files) {
    if (!file.isEmpty()) {
        map.add("images", file.getResource());
    }
}

[...]
Share:
96,531
Mateusz Mańka
Author by

Mateusz Mańka

Updated on July 09, 2022

Comments

  • Mateusz Mańka
    Mateusz Mańka almost 2 years

    I am trying to upload a file with RestTemplate to Raspberry Pi with Jetty. On Pi there is a servlet running:

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
    
        PrintWriter outp = resp.getWriter();
    
        StringBuffer buff = new StringBuffer();
    
        File file1 = (File) req.getAttribute("userfile1");
        String p = req.getParameter("path");
        boolean success = false;
    
        if (file1 == null || !file1.exists()) {
            buff.append("File does not exist\n");
        } else if (file1.isDirectory()) {
            buff.append("File is a directory\n");
        } else {
            File outputFile = new File(req.getParameter("userfile1"));
            if(isValidPath(p)){
                p = DRIVE_ROOT + p;
                final File finalDest = new File(p
                        + outputFile.getName());
                success = false;
                try {
                    copyFileUsingFileChannels(file1, finalDest);
                    finalDest.setWritable(true);
                    success = true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (success){
                    buff.append("File successfully uploaded.\n");
                }
                else{
                                        buff.append("Failed to save file.");
                }
            }
            else{
                buff.append("Invalid path.\n");
            }
        }
        outp.write(buff.toString());
    }
    

    I am able to successfully do it with curl

    curl --form userfile1=@/home/pi/src/CreateNewFolderServlet.java --form press=OK localhost:2222/pi/GetFileServlet?path="/media/"

    This is the method that is supposed to have the same functionality on webapp.

    @ResponseBody 
    @RequestMapping(value="/upload/",method=RequestMethod.POST ,produces = "text/plain")
    public String uploadFile(MultipartHttpServletRequest request2, HttpServletResponse response2){
    
        Iterator<String> itr =  request2.getFileNames();
    
         MultipartFile file = request2.getFile(itr.next());
         System.out.println(file.getOriginalFilename() +" uploaded!");
    
        System.out.println(file.toString()); 
         MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
        parts.add("userfile1",file);
        //reqEntity.addPart("userfile1", file);
        String path="/public/";
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        System.out.println("1");
        HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(parts, headers);
        String url =  url2+"/pi/GetFileServlet?path="+path;
        System.out.println("2");
    /*  restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
        restTemplate.getMessageConverters().add(
                new MappingJackson2HttpMessageConverter());*/
        System.out.println("3");
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request,String.class);
        System.out.println("4");
        System.out.println("response : " +response);
        if(response==null||response.getBody().trim()==""){
            return "error";
        }
        return response.getBody();
    }
    

    This is the output that I get:

    ui-elements.html uploaded!

    org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@47e7673e

    1

    2

    3

    As you can see number 4 is not printed No exception in console. Exceptions found during debugging:

    org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"])
    
  • chrismarx
    chrismarx over 8 years
    for spring 3.1 & 3.2 at least, I also needed to deal with the bug in resttemplate when sending byte arrays - stackoverflow.com/questions/4118670/…
  • Tharsan Sivakumar
    Tharsan Sivakumar over 7 years
    You saved me at last. Yes Spring MVC perfectly working for me
  • Manish Singh
    Manish Singh over 6 years
    Surprisingly, the accepted answer didn't work for me, but this one worked! Thanks.
  • Krish
    Krish over 6 years
    @lorenzo-polidori Can you please provide example controller method on how to receive MultipartInputStreamFileResource ? i.e., sample controller method for InputStreamResource
  • Brent Bradburn
    Brent Bradburn almost 6 years
    I have used this excellent approach, with some improvements (including further reduced memory consumption) -- and posted a followup here: Streaming upload via @Bean-provided RestTemplateBuilder buffers full file.
  • Deep Lathia
    Deep Lathia about 5 years
    I tried this method but did not work for me. I am facing an issue making a POST request with the data in multipart format. Here's my question if you could guide me with a solution stackoverflow.com/questions/54429549/…
  • Deep Lathia
    Deep Lathia about 5 years
    I tried this method but did not work for me. I am facing an issue making a POST request with the data in multipart format. Here's my question if you could guide me with a solution stackoverflow.com/questions/54429549/…
  • RobOhRob
    RobOhRob about 5 years
    You're a genius. I couldn't get anything to work but this solution did the trick.
  • hzpz
    hzpz over 4 years
    Spring ships with its own MultipartFileResource now. See my answer for the details.
  • Ravindra Ranwala
    Ravindra Ranwala about 3 years
    Exactly, that worked perfectly for me. You saved my day. Thanks !