I have the following endpoind:
// inside @RestController annotated class
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> upload(@RequestParam Map<String, MultipartFile> upfiles) throws Exception {
return ResponseEntity.ok(new Object());
}
In another microservice I created a feign client to trigger the above endpoint:
// inside @FeginClient annotated class
@PostMapping(
value = "/upload",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
ResponseEntity<UploadResponse> uploadDocumentToSign(@Valid @RequestParam Map<String, MultipartFile> upfiles);
The above call fails with a message “Failed to parse multipart servlet request”
So I tried from postman, the only way it works from postman is as follows:
As you can see the Map is placed on the body as a form-data not a query param as it is implied by the method definition
So I tried the same approach with feign client:
//inside @FeginClient annotated class
@PostMapping(
value = "/upload",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<UploadResponse> uploadDocumentToSign(@Valid @RequestBody Map<String,
MultipartFile> upfiles);
But this did not work as well, the request was reaching the server but the Map was empty in the Controller class, even after creating an encoder Bean of type SpringFormEncoder
.
It seems that SpringFormEncoder
does not know how to handle a multipart/form-data request with a body of type Map<String, MultipartFile> so I had to create my own SpringFormEncoder
:
public class DocumentUploadSpringFormEncoder extends SpringFormEncoder {
public DocumentUploadSpringFormEncoder() {
}
public DocumentUploadSpringFormEncoder(Encoder delegate) {
super(delegate);
}
@Override
public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (bodyType.equals(MultipartFile[].class)) {
val files = (MultipartFile[]) object;
val data = new HashMap<String, Object>(files.length, 1.F);
for (val file : files) {
data.put(file.getName(), file);
}
super.encode(data, MAP_STRING_WILDCARD, template);
} else if (bodyType.equals(MultipartFile.class)) {
val file = (MultipartFile) object;
val data = singletonMap(file.getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
} else if (isMultipartFileCollection(object)) {
val iterable = (Iterable<?>) object;
val data = new HashMap<String, Object>();
for (val item : iterable) {
val file = (MultipartFile) item;
data.put(file.getName(), file);
}
super.encode(data, MAP_STRING_WILDCARD, template);
} else if (isMultipartFileMap(object)) {
val map = (Map<?, ?>) object;
val entrySet = map.entrySet();
val data = new HashMap<String, Object>();
for (Map.Entry entry : entrySet) {
val file = (MultipartFile) entry.getValue();
val key = (String) entry.getKey();
data.put(key, file);
}
super.encode(data, MAP_STRING_WILDCARD, template);
}
else {
super.encode(object, bodyType, template);
}
}
private boolean isMultipartFileCollection (Object object) {
if (!(object instanceof Iterable<?> iterable)) {
return false;
}
val iterator = iterable.iterator();
return iterator.hasNext() && iterator.next() instanceof MultipartFile;
}
private boolean isMultipartFileMap (Object object) {
if (!(object instanceof Map<?, ?> map)) {
return false;
}
val entrySet = map.entrySet();
for (Map.Entry entry : entrySet) {
if (!(entry.getValue() instanceof MultipartFile)) {
return false;
}
}
return true;
}
}
And only now everything worked.
My questions are:
- Is this the way to accept a Map<String, MultipartFile>
@RequestParam Map<String, MultipartFile> upfiles
, why @RequestParam when you actually need to put this in the body ? - If it is the correct why you define it one way in the Controller and another way on the client side useing spring openfegin (still pring framework…)