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.
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;
}