Search code examples
resttemplatespring-restspring-resttemplatespring5

Spring 5: use RestTemplate to POST multiple files with POJO; MediaType.MULTIPART_FORM_DATA with LinkedMultiValueMap


Versions:

Java 11
Spring 5.3.9
Jackson 2.13
org.apache.httpcomponents:httpclient: 4.5.13

I believe the issue here is that out-of-the-box, RestTemplate cannot deal with an HttpEntity for which the value is itself a MultiValueMap with (String, Resource) pairs. How to resolve that? I suppose the canonical use-case is supporting the upload of multiple files concurrently through an HTML form, along with meta-data. Details follow.

Here are message converters:

  private List<HttpMessageConverter<?>> getMessageConverters()
  {
    List<MediaType> mediaTypes = new ArrayList<>();
    mediaTypes.add(MediaType.TEXT_HTML);
    mediaTypes.add(MediaType.APPLICATION_JSON);
    mediaTypes.add(MediaType.TEXT_PLAIN);

    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(mediaTypes);

    List<MediaType> formMediaTypes = new ArrayList<>();
    formMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
    formMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

    FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
    formConverter.setSupportedMediaTypes(formMediaTypes);
    formConverter.addPartConverter(new MappingJackson2HttpMessageConverter());
    formConverter.addPartConverter(new ResourceHttpMessageConverter());

    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setSupportedMediaTypes(formMediaTypes);

    List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    messageConverters.add(converter);
    messageConverters.add(formConverter);
    messageConverters.add(stringConverter);
    return messageConverters;
  }

And:

    RestTemplate restTemplate = new RestTemplate(requestFactory);
    restTemplate.setMessageConverters(getMessageConverters());

Then I create one HttpEntity with the files (in this example, I am only POSTing a single file):

    ByteArrayResource bas = new ByteArrayResource(labxReport.getPDFFile().getBytes()) {
      @Override public String getFilename() { return reportFilename; }
    };
    MultiValueMap<String, Object> reportFiles = new LinkedMultiValueMap<String, Object>();
    reportFiles.add(reportFilename, bas);

    HttpHeaders reportFilesReqHeaders = new HttpHeaders();
    reportFilesReqHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    HttpEntity<MultiValueMap<String, Object>> reportFilesEntity = new HttpEntity<>(reportFiles, reportFilesReqHeaders);

For the POJO (an instance of ReportInfo here), I create a separate HttpEntity like this:

    HttpHeaders reportInfoReqHeaders = new HttpHeaders();
    reportInfoReqHeaders.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<ReportInfo> reportInfoEntity = new HttpEntity<>(reportInfo, reportInfoReqHeaders);

Then I cobble together the HttpEntity for my main POST request like this:

    MultiValueMap<String, Object> postParams = new LinkedMultiValueMap<String, Object>();
    postParams.set("files", reportFilesEntity);
    postParams.set("data", reportInfoEntity);

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
    httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

    HttpEntity<MultiValueMap<String, Object>> requestPOST = new HttpEntity<>(postParams, httpHeaders);

Finally, I make the POST request:

    ResponseEntity<String> response = restTemplate.exchange(PORTAL_URL, HttpMethod.POST, requestPOST, String.class);

This results in the following stack trace:

org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.util.LinkedMultiValueMap]
        at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:532) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.writeParts(FormHttpMessageConverter.java:503) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.writeMultipart(FormHttpMessageConverter.java:483) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:360) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:156) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:950) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:672) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:581) ~[spring-web-5.2.16.RELEASE.jar:5.2.16.RELEASE]
        ...

Thanks.


Solution

  • The way I resolved this issue is shown below in getMessageConverters. The thinking was that the files form parameter was itself a sort of form-based message (ie, with key/value pairs), and therefore required another instance of FormHttpMessageConverter.

    Here's the logic that worked for me:

      private List<HttpMessageConverter<?>> getMessageConverters()
      {
        List<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.TEXT_HTML);
        mediaTypes.add(MediaType.APPLICATION_JSON);
        mediaTypes.add(MediaType.TEXT_PLAIN);
    
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(mediaTypes);
    
        List<MediaType> formMediaTypes = new ArrayList<>();
        formMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        formMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
    
        FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
        formConverter.setSupportedMediaTypes(formMediaTypes);
        formConverter.addPartConverter(new MappingJackson2HttpMessageConverter());
    
        FormHttpMessageConverter multifileConverter = new FormHttpMessageConverter();
        multifileConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM));
        multifileConverter.addPartConverter(new ResourceHttpMessageConverter());
        formConverter.addPartConverter(multifileConverter);
    
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setSupportedMediaTypes(formMediaTypes);
    
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(converter);
        messageConverters.add(formConverter);
        messageConverters.add(stringConverter);
        return messageConverters;
      }