How to enable Spring Reactive Web MVC to handle Multipart-file? - spring

I'm trying to use the new reactive web-mvc implementation in a spring boot 2.0 application. I'm trying to define a method which consume multipart file but do not succeed at making it working :( - I always get a 415 error.
On one hand I have a controller containing the following request mapping :
#RequestMapping(method = RequestMethod.POST, path = "/myPath/{param}/{param2}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#ResponseBody
public Mono<Void> postFile(
#RequestBody MultipartFile data,
#PathVariable("param") String param,
#PathVariable("param2") String param2,
#RequestHeader(name = HEADER_DATE, required = false) #DateTimeFormat(pattern = DATE_FORMAT) Instant instant
){
return fileService.handleData(Mono.just(data), param, param2, instant);
}
On the other hand I had to add a server on the top of the basic dependencies as it seems netty do not handle multipart files. I so added the spring-boot-starter-tomcatdependency which enabled the MultipartAutoConfiguration to be matched and satisfied on application auto configuration.
When posting something using a curl call :
curl 'Meta-Date: 20170101104532' --form "file=#file.bin" http://localhost:8082/myPath/foo/bar
while debug logs are activated (logging.level.org.springframework.web=DEBUG) I got this exception :
org.springframework.web.server.UnsupportedMediaTypeStatusException: Request failure [status: 415, reason: "Content type 'multipart/form-data;boundary=------------------------58fa43b8f1a26de4' not supported"]
This error is thrown by the RequestBodyArgumentResolver which has the the following supported media types : [*/*, text/xml, application/*+json;charset=UTF-8, application/xml, text/plain;charset=UTF-8, application/x-www-form-urlencoded, application/json;charset=UTF-8] provided by 9 DecoderHttpMessageReader.
Before posting I also took a look at :
Spring MultiPart MediaType Unsupported which seems to not be relevant here as my autoconf report contains the following entry : MultipartAutoConfiguration#multipartResolver matched
set content-type to utf-8 with angularjs $http Adding a header setting Content-Transfer-Encoding: binary didn't changed anything.
My understanding is that Spring web 5.0 uses a new request decoder system as I don't find these classes on a spring 4 spring boot application, and there is not yet any DecoderHttpMessageReader dealing with multipart file
Did I miss something ? Or should I wait one to be implemented ?

Okay, It seems this is just not implemented for now as it currently exists a pull request for this feature : Add reactive multipart request support #1201
Should have check this earlier...
[EDIT] : The issue has been solved and merged into Spring master branch. Should no longer be an issue.

#PutMapping(value="/{..}",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<Void> save(#RequestPart("file") FilePart multipartFormData,#RequestParam("fileName") String fileName,#PathVariable("..") String ..) throws IOException {
List<ByteBuffer> bytesList = new LinkedList<>();
multipartFormData.content().
subscribe(item->bytesList.add(item.asByteBuffer()));
int totalBytes = bytesList.stream().mapToInt(item->item.capacity()).sum();
ByteBuffer buffer = ByteBuffer.allocate(totalBytes);
bytesList.stream().forEach(byteBuff->buffer.put(byteBuff));
baseImageHandler.saveImage(buffer, fileName, baseItemId);
return Mono.empty();
}
Please note that it is a dev verison, but this is how I have managed to do it.

Related

Springfox swagger - content type multipart/form-data

I have an API for uploading multiple files with below signature - takes in a list of multipart files and a request object.
#ApiOperation(consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
#PostMapping("/upload")
public void uploadMultipleFiles(#RequestParam("req") RequestDTO request, #RequestParam("files") List<MultipartFile> files) {}
When I test this API using Postman it works but I when try using swagger, I noticed the content-type is passed as application/json and the API gives an error 'Current request is not multpart'. I tried adding consumes to #ApiOperation but the content-type is still application/json.
Files in OpenAPI 3.x are better represented by application/octet-stream.
I solved the issue using the below approach
#Operation( // Swagger/OpenAPI 3.x annotation to describe the endpoint
summary = "Small summary of the end-point",
description = "A detailed description of the end-point"
)
#PostMapping(
value = "/uploads",
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE} // Note the consumes in the mapping
)
public void uploadMultipleFiles (
// ------------ Multipart Object ------------
#Parameter(description = "Additional request data") // Swagger/OpenAPI annotation
#RequestParam("req") RequestDTO request, // Spring annotation
// ------------ File uploads go next ------------
#Parameter(
description = "Files to be uploaded",
content = #Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE) // Won't work without OCTET_STREAM as the mediaType.
)
#RequestParam("files") List<MultipartFile> files // Spring annotation
)
More details for file upload in the OpenAPI 3 specification can be found here

How to migrate this mutlipart file upload to Spring Boot 2.4?

With Spring Boot 2.3 I was using the following Kotlin code
val mvcResultImage = this.mockMvc!!.perform(MockMvcRequestBuilders.multipart("/somepath)
.file("files[]", imageFile.getBytes())
.characterEncoding("UTF-8"))
.andReturn()
in an integration test for a controller with a function
#PostMapping(path = ["/somepath"],
consumes = [MediaType.MULTIPART_FORM_DATA_VALUE],
produces = [MediaType.APPLICATION_JSON_VALUE])
#ResponseBody
fun createFromBytes(#RequestParam("files[]") file: MultipartFile): ResponseEntity<Any> {
...
}
In 2.3 I was able to handle the request in the controller function whereas in 2.4 the controller function raises a org.springframework.web.multipart.support.MissingServletRequestPartException with the message Required request part 'files[]' is not present and causes HTTP response code 400.
I don't find anything in the migration guide and list of handled issues for this version change.
A rename to file in both controller and request doesn't help, I don't remember why I added [] in the code working with 2.3, but I think it was necessary to make it work.
I'm using Spring Boot through the maven parent mechanism with spring-boot-starter-parent:2.4.1.
This is a known issues in Spring Boot coming from Spring. It's fixed in Spring Boot 2.4.2. The linked issue contains a successfully tested workaround in case you're stuck with 2.4.1: Create MockMultipartFile with MockMultipartFile( String name, #Nullable String originalFilename, #Nullable String contentType, #Nullable byte[] content) (specification of originalFilename matters).

How to do java unit test with protobuf for controller?

I have a spring boot rest controller with requestBody & responseBody both protobuf. like below :
#RequestMapping(value = "/position/open", produces = "application/x-protobuf")
#ResponseBody
public MsgProto.Response positionOpen(#RequestBody MsgProto.Request request)throws Exception {
log.info("start /position/open");
return orderPositionService.addOrder(request);
}
Now I want to do a unit test using mockMvc to test the controller, but it failed every time. I believe it is the code below which is wrong to fire an HTTP request with protobuf, any idea how to resolve it?
mockMvc.perform(post("/position/open").contentType("application/x-protobuf")
.content(ObjectsMock.mockMsgProtoRequest().toByteArray())).andDo(print())
.andExpect(status().isOk());
Exception :
Resolved Exception:
Type = org.springframework.web.HttpMediaTypeNotSupportedException
MockHttpServletResponse:
Status = 415
Error message = null
Headers = [Accept:"application/json, application/octet-stream,
application/xml, application/*+json, text/plain, text/xml, application/x-www-
form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, */*"]
I assume the ProtobufHttpMessageConverter is missing here. Spring MVC can't read/write any messages without this specific converter.
You can create it as the following:
#Bean
public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
Next, make sure to add the HTTP Method to your method, as I assume (from reading your test) you want this to be a HTTP POST handler. You can also add the consumes attribute to state that this endpoint also consumes Protobuf.
#RequestMapping(method = RequestMethod.POST, consumes = "application/x-protobuf", value = "/position/open", produces = "application/x-protobuf")
#ResponseBody
public MsgProto.Response positionOpen(#RequestBody MsgProto.Request request)throws Exception {
log.info("start /position/open");
return orderPositionService.addOrder(request);
}
In addition to this, there is an article on the Spring blog available that covers your usecase and explains how to use Protobuf with Spring MVC.
You need to add Protobuf converter to MockMvc builder
MockMvcBuilders.standaloneSetup(controller)
.setMessageConverters(new ProtobufHttpMessageConverter())
.build()
This fixed the issue for me

How to create an Open API 3.0.1 Specification

I am new to swagger documentation etc
Please, could you share any good resource or steps for creating an open api spec for the following endpoint, which is an endpoint of a spring boot microservice:
#PostMapping(path = "/pdf", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<ByteArrayResource> createReport(#RequestParam MultipartFile template, #RequestParam MultipartFile templateDataAsJson) throws IOException {
log.info("Triggering PDF Generation and Download");
log.info("Step 1 Starts : Sending Json data to the template data binder microservice: Request:{}", templateDataAsJson);
String completedHtmlJson = restClient.populateTemplate(template, templateDataAsJson);
log.info("Steps 2 Starts: Sending populated html template to html-to-pdf microservice for rendering:{}", completedHtmlJson);
ResponseEntity<ByteArrayResource> response = restClient.html2PdfGeneration(completedHtmlJson);
return ResponseEntity.ok().contentType(APPLICATION_PDF).body(response.getBody());
}
Any help or references will be appreciated.
Thanks all.
You can look at SpringDoc
https://github.com/springdoc/springdoc-openapi
It will generate the documentation on the fly for for you.

Cannot get Spring Boot to lazily resolve a multipart file

I have created a Spring Boot 2 demo application with the Spring Initializr and added the controller below:
#Controller
#RequestMapping("/demo")
public class UploadController {
private final static Logger LOG = LoggerFactory.getLogger(UploadController.class);
#PostMapping("/upload")
public ResponseEntity<String> uploadFile(
#RequestParam("metadata") MultipartFile metadata,
#RequestParam("payload") MultipartFile payload) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map metadataMap = mapper.readValue(metadata.getInputStream(), Map.class);
LOG.info("Received call to upload file {}", metadataMap.get("filename"));
LOG.info("File size: {}", payload.getBytes().length);
LOG.info("File {} successfully uploaded", metadataMap.get("filename"));
return ResponseEntity.ok().build();
}
}
I then added an application.yaml file containing this configuration:
spring:
servlet:
multipart:
max-file-size: 2000000MB
max-request-size: 2000000MB
resolve-lazily: true
My goal is to have the controller parse and log the metadata file before it starts reading the payload file, but the resolve-lazily setting seems to be ignored by Boot: the code inside the controller won't be executed until the whole body is read.
I use the command below to test the controller:
curl -F metadata=#metadata.json -F payload=#payload.bin http://localhost:8080/demo/upload
Is there anything wrong with my code/configuration? Am I getting the meaning of the setting right?
At present, if you want to avoid reading (and buffering) the whole body all at once, I think you will have to provide your own parser, as described in the answers here. What would be really interesting (but generally unnecessary) would be to do so in the form of a new MultipartResolver implementation.
There are two existing implementations documented for interface MultipartResolver, and both supply a function setResolveLazily(boolean) (standard), (commons). I have tried with both, and neither seem to allow for parsing or streaming multipart files or parameters independently.
Default is "false", resolving the multipart elements immediately, throwing corresponding exceptions at the time of the resolveMultipart(javax.servlet.http.HttpServletRequest) call. Switch this to "true" for lazy multipart parsing, throwing parse exceptions once the application attempts to obtain multipart files or parameters.
Despite what it says in the documentation, I have found that once you call resolveMultipart, the entire body is parsed and buffered before the call returns. I know this because I can watch the temp-files being created.
One note about "Is there anything wrong with my code"...
Answer: Yes, because by using #RequestParam you have indirectly asked Spring to resolve your parameters ahead of time, before your controller is ever called. What you should be able to do instead (if the documentation were correct) is request the parameters independently from inside your controller:
Configuration (application.properties):
spring.servlet.multipart.enabled = true
spring.servlet.multipart.resolve-lazily = true
Controller:
#PostMapping(path = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> postUpload(HttpServletRequest rawRequest) {
multipartResolver.setResolveLazily(true); // unclear why this is exists
MultipartHttpServletRequest request = multipartResolver.resolveMultipart(rawRequest);
String p1 = request.getParameter("first-parameter");
String p2 = request.getParameter("second-parameter");
System.out.println("first-parameter="+p1+", second-parameter"+p2);
multipartResolver.cleanupMultipart(request);
return new ResponseEntity<Void>(HttpStatus.ACCEPTED);
}
One useful aspect of resolve-lazily that I have discovered is that it allows you to write your own parser for some rest controllers while using the built-in parser for others (see my answer here). In other words, you don't have to use spring.servlet.multipart.enabled = false to get your parser to work. This is a minor breakthrough relative to other advice that I had seen previously.

Resources