I want to receive Multipart/form-data from a client (frontend for example). And then stream file content of form-data to another backend service.
For now i can read the whole file and pass it somewhere via byte[] (base64 string) like this:
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseType> upload(#RequestPart("document") FilePart document,
#RequestPart("stringParam") String stringParam) {
return service.upload(document, stringParam);
}
// Casually convert to single byte array...
private Mono<byte[]> convertFilePartToByteArray(FilePart filePart) {
return Mono.from(filePart
.content()
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}));
}
There're a few problems with this approach:
I don't want to read the whole file into memory;
Array size in limited to Integer.MAX_VALUE;
Array encodes as base64 String, which takes extra memory;
Since i put the whole array in Mono - "spring.codec.max-in-memory-size" must be bigger than array size.
I've already tried sending file via asyncPart of WebClientBuilder:
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.asyncPart("document", document.content(), DataBuffer.class);
But i'm getting an error:
java.lang.IllegalStateException: argument type mismatch
Method [public reactor.core.publisher.Mono<> upload(**org.springframework.http.codec.multipart.FilePart**,java.lang.String)] with argument values:
[0] [type=**org.springframework.http.codec.multipart.DefaultParts$DefaultFormFieldPart**]
UPD: full code, which generates error
// External controller for client.
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/v2")
public Mono<DocumentUploadResponse> uploadV2(#RequestPart("document") FilePart document,
#RequestPart("stringParam") String stringParam) {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.asyncPart("document", document.content(), DataBuffer.class);
builder.part("stringParam", stringParam);
WebClient webClient = webClientBuilder.build();
return webClient.post()
.uri("URL_TO_ANOTHER_SERVICE")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(FileMetaDto.class)
.map(DocumentUploadResponse::new);
}
// Internal service controller.
#PostMapping(path = "/upload/v2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<FileMetaDto> upload(#RequestPart("document") FilePart document,
#RequestPart("stringParam") String stringParam) {
return ...;
}
Looks like i was managed to stream file, working code below:
// External controller for client.
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/v2")
public Mono<DocumentUploadResponse> uploadV2(#RequestPart("document") FilePart document,
#RequestPart("stringParam") String stringParam) {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.asyncPart("document", document.content(), DataBuffer.class).filename(document.filename());
builder.part("stringParam", stringParam);
WebClient webClient = webClientBuilder.build();
return webClient.post()
.uri("URL_TO_ANOTHER_SERVICE")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(FileMetaDto.class)
.map(DocumentUploadResponse::new);
}
// Internal service controller.
#PostMapping(path = "/upload/v2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<FileMetaDto> upload(#RequestPart("document") FilePart document,
#RequestPart("stringParam") String stringParam) {
return ...;
}
In the original question code i've been missing:
builder.asyncPart("document", document.content(), DataBuffer.class).filename(document.filename());
Related
I'm new to MockMVC. I've successfully written some basic tests, but I got stuck on trying to test an use case with the endpoint that requires a POST request with two parameters - a POJO and an array of MultipartFile. The test is written as such:
#Test
public void vytvorPodnetTest() throws Exception {
var somePojo = new SomePojo();
somePojo.setSomeVariable("test_value");
var roles = List.of("TEST_USER");
var uid = "00000000-0000-0000-0000-000000000001";
MockMultipartFile[] attachments = {new MockMultipartFile("file1.txt", "file1.txt", "text/plain", "file1 content".getBytes()),
new MockMultipartFile("file2.txt", "file2.txt", "text/plain", "file2 content".getBytes())};
MockMultipartHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart("/some-pojo/create");
builder.with(req - {
req.setMethod("POST");
return req;
});
MvcResult result = mockMvc.perform(builder.file(attachments[0]).file(attachments[1])
.param("SomePojo", new ObjectMapper().writeValueAsString(somePojo))
.file(attachment[0])
.with(TestUtils.generateJWTToken(uid, roles)))
.andExpect(status.isOk())
.andReturn();
}
The controller method is as follows:
#PostMapping(value = "/create", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public UUID createPojo(
#RequestPart(value = "SomePojo") SomePojo somePojo,
#RequestPart(value = "attachments", required = false) MultipartFile[] attachments) {
return pojoService.create(somePojo, attachments);
}
It stops here, before reaching the service. I've tried adding the files both as a param "attachments" and like shown above, but all I get is "400 Bad Request"
Finally found the way to send the parameters as MockMultipartFile from MockMVC to the controller:
MockMultipartFile pojoJson = new MockMultipartFile("SomePojo", null,
"application/json", JsonUtils.toJSON(podnet).getBytes());
mockMvc.perform(MockMvcRequestBuilders.multipart("/some-pojo/create")
.file(pojoJson)
.contentType(MediaType.MULTIPART_FORM_DATA_VALUE)
.with(new TestUtils().generateJWTToken(uid, roles)))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
What should be in the consumes and produces for Rest API which is returning byte[] of file in the response.
No file params are included in the request .
You could use the below for returning byte[]
#Produces(MediaType.APPLICATION_OCTET_STREAM)
You can use 'MultipartFile' for the purpose of consuming and sending back a file in response.
You can have a look at the following tutorial at spring.io for detailed tutorial:
https://spring.io/guides/gs/uploading-files/
Hope it helps!
You should set the media type on basis of file content type.
for example:
#GetMapping
public HttpEntity returnByteArray() {
String filepath = ; //filepath
String contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(filePath);
byte[] byteContent = ; //Content
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf(contentType));
return new HttpEntity(byteContent, headers);
}
OR
If you always return the same content file type then you can also set in
#GetMapping(produces = "mime_type")
public byte[] returnByteArray() {
return new byte[0];
}
How to upload multiple files using Webflux?
I send request with content type: multipart/form-data and body contains one part which value is a set of files.
To process single file I do it as follow:
Mono<MultiValueMap<String, Part> body = request.body(toMultipartData());
body.flatMap(map -> FilePart part = (FilePart) map.toSingleValueMap().get("file"));
But how to done it for multiple files?
PS. Is there another way to upload a set of files in webflux ?
I already found some solutions.
Let's suppose that we send an http POST request with an parameter files which contains our files.
Note responses are arbitrary
RestController with RequestPart
#PostMapping("/upload")
public Mono<String> process(#RequestPart("files") Flux<FilePart> filePartFlux) {
return filePartFlux.flatMap(it -> it.transferTo(Paths.get("/tmp/" + it.filename())))
.then(Mono.just("OK"));
}
RestController with ModelAttribute
#PostMapping("/upload-model")
public Mono<String> processModel(#ModelAttribute Model model) {
model.getFiles().forEach(it -> it.transferTo(Paths.get("/tmp/" + it.filename())));
return Mono.just("OK");
}
class Model {
private List<FilePart> files;
//getters and setters
}
Functional way with HandlerFunction
public Mono<ServerResponse> upload(ServerRequest request) {
Mono<String> then = request.multipartData().map(it -> it.get("files"))
.flatMapMany(Flux::fromIterable)
.cast(FilePart.class)
.flatMap(it -> it.transferTo(Paths.get("/tmp/" + it.filename())))
.then(Mono.just("OK"));
return ServerResponse.ok().body(then, String.class);
}
You can iterate hashmap with Flux and return Flux
Flux.fromIterable(hashMap.entrySet())
.map(o -> hashmap.get(o));
and it will be send as an array with filepart
the key is use toParts instead of toMultipartData, which is more simpler. Here is the example that works with RouterFunctions.
private Mono<ServerResponse> working2(final ServerRequest request) {
final Flux<Void> voidFlux = request.body(BodyExtractors.toParts())
.cast(FilePart.class)
.flatMap(filePart -> {
final String extension = FilenameUtils.getExtension(filePart.filename());
final String baseName = FilenameUtils.getBaseName(filePart.filename());
final String format = LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE);
final Path path = Path.of("/tmp", String.format("%s-%s.%s", baseName, format, extension));
return filePart.transferTo(path);
});
return ServerResponse
.ok()
.contentType(APPLICATION_JSON_UTF8)
.body(voidFlux, Void.class);
}
希望对你有帮助
#PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public JSON fileUpload(#RequestPart FilePart file)throws Exception{
OSS ossClient = new OSSClientBuilder().build(APPConfig.ENDPOINT, APPConfig.ALI_ACCESSKEYID, APPConfig.ALI_ACCESSSECRET);
File f = null;
String url;
try {
String suffix = file.filename();
String fileName = "images/" + file.filename();
Path path = Files.createTempFile("tempimg", suffix.substring(1, suffix.length()));
file.transferTo(path);
f = path.toFile();
ossClient.putObject(APPConfig.BUCKETNAME, fileName, new FileInputStream(f));
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10);
url = ossClient.generatePresignedUrl(APPConfig.BUCKETNAME, fileName, expiration).toString();
}finally {
f.delete();
ossClient.shutdown();
}
return JSONUtils.successResposeData(url);
}
Following is the working code for uploading multiple files using WebFlux.
#RequestMapping(value = "upload", method = RequestMethod.POST)
Mono<Object> upload(#RequestBody Flux<Part> parts) {
return parts.log().collectList().map(mparts -> {
return mparts.stream().map(mmp -> {
if (mmp instanceof FilePart) {
FilePart fp = (FilePart) mmp;
fp.transferTo(new File("c:/hello/"+fp.filename()));
} else {
// process the other non file parts
}
return mmp instanceof FilePart ? mmp.name() + ":" + ((FilePart) mmp).filename() : mmp.name();
}).collect(Collectors.joining(",", "[", "]"));
});
};
I'm trying to use the Spring Reactive WebClient to upload a file to a spring controller. The controller is really simple and looks like this:
#PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> uploadFile(
#RequestParam("multipartFile") MultipartFile multipartFile,
#RequestParam Map<String, Object> entityRequest
) {
entityRequest.entrySet().forEach(System.out::println);
System.out.println(multipartFile);
return ResponseEntity.ok("OK");
}
When I use this controller with cURL everything works fine
curl -X POST http://localhost:8080/upload -H 'content-type: multipart/form-data;' -F fileName=test.txt -F randomKey=randomValue -F multipartFile=#document.pdf
The multipartFile goes to the correct parameter and the other parameters go in to the Map.
When I try to do the same from the WebClient I get stuck. My code looks like this:
WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.set("multipartFile", new ByteArrayResource(Files.readAllBytes(Paths.get("/path/to/my/document.pdf"))));
map.set("fileName", "test.txt");
map.set("randomKey", "randomValue");
String result = client.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.syncBody(map)
.exchange()
.flatMap(response -> response.bodyToMono(String.class))
.flux()
.blockFirst();
System.out.println("RESULT: " + result);
This results in an 400-error
{
"timestamp":1510228507230,
"status":400,
"error":"Bad Request",
"message":"Required request part 'multipartFile' is not present",
"path":"/upload"
}
Does anyone know how to solve this issue?
So i found a solution myself. Turns out that Spring really needs the Content-Disposition header to include a filename for a upload to be serialized to a MultipartFile in the Controller.
To do this i had to create a subclass of ByteArrayResource that supports setting the filename
public class MultiPartResource extends ByteArrayResource {
private String filename;
public MultiPartResource(byte[] byteArray) {
super(byteArray);
}
public MultiPartResource(byte[] byteArray, String filename) {
super(byteArray);
this.filename = filename;
}
#Nullable
#Override
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
}
Which can then be used in the client with this code
WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.set("fileName", "test.txt");
map.set("randomKey", "randomValue");
ByteArrayResource resource = new MultiPartResource(Files.readAllBytes(Paths.get("/path/to/my/document.pdf")), "document.pdf");
String result = client.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(map))
.exchange()
.flatMap(response -> response.bodyToMono(String.class))
.flux()
.blockFirst();
System.out.println("RESULT: " + result);
You need include a filename to file part to upload success, in combination with asyncPart() to avoid buffering all file content, then you can write the code like this:
WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();
Mono<String> result = client.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body((outputMessage, context) ->
Mono.defer(() -> {
MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();
Flux<DataBuffer> data = DataBufferUtils.read(
Paths.get("/tmp/file.csv"), outputMessage.bufferFactory(), 4096);
bodyBuilder.asyncPart("file", data, DataBuffer.class)
.filename("filename.csv");
return BodyInserters.fromMultipartData(bodyBuilder.build())
.insert(outputMessage, context);
}))
.exchange()
.flatMap(response -> response.bodyToMono(String.class));
System.out.println("RESULT: " + result.block());
Easier way to provide the Content-Disposition
MultipartBodyBuilder builder = new MultipartBodyBuilder();
String header = String.format("form-data; name=%s; filename=%s", "paramName", "fileName.pdf");
builder.part("paramName", new ByteArrayResource(<file in byte array>)).header("Content-Disposition", header);
// in the request use
webClient.post().body(BodyInserters.fromMultipartData(builder.build()))
Using a ByteArrayResource in this case is not efficient, as the whole file content will be loaded in memory.
Using a UrlResource with the "file:" prefix or a ClassPathResource should solve both issues.
UrlResource resource = new UrlResource("file:///path/to/my/document.pdf");
I have an rest API in a Spring for generating and downloading a PDF file. The controller definitation is as follows -
#RequestMapping(
value = "/foo/bar/pdf",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
#ResponseBody
#Nullable
public ByteArrayResource downloadPdf(#RequestParam int userId) {
byte[] result = null;
ByteArrayResource byteArrayResource = null;
result = service.generatePdf(userId);
if (result != null) {
byteArrayResource = new ByteArrayResource(result);
}
return byteArrayResource;
}
I use Jackson for JSON handling JSON and have an Exception handler ControllerAdvice. The problem is when this API generates an exception and I return a custom exception class (contains message and one additional field).
As I already specified produces = MediaType.APPLICATION_OCTET_STREAM_VALUE this custom class is also attempted to be converted to an octet stream by Spring, which it fails at and produces HttpMediaTypeNotAcceptableException: Could not find acceptable representation.
I tried solutions on this Stackoverflow question, particularly this answer but it still fails. This solution, along with other changes suggests removing produces part from #RequestMapping but when I debugged into AbstractMessageConverterMethodProcessor.getProducibleMediaTypes it only detects application/json as available response media type.
tl;dr
How can I have this API return the file on success and correctly return custom exception class's JSON representation on error.
I had the same problem with similar code. I just removed the produces attribute from my #PostMapping and I was able to return the file or the json (when the api have some error):
#Override
#PostMapping
public ResponseEntity<InputStreamResource> generate(
#PathVariable long id
) {
Result result = service.find(id);
return ResponseEntity
.ok()
.cacheControl(CacheControl.noCache())
.contentLength(result.getSize())
.contentType(MediaType.parseMediaType(MediaType.APPLICATION_PDF_VALUE))
.body(new InputStreamResource(result.getFile()));
}
When some error occur, I had a #ExceptionHandler to care of that:
#ExceptionHandler
public ResponseEntity<ApiErrorResponse> handleApiException(ApiException ex) {
ApiErrorResponse error = new ApiErrorResponse(ex);
return new ResponseEntity<>(error, ex.getHttpStatus());
}
Try implements your action as
#RequestMapping(
value = "/foo/bar/pdf",
method = RequestMethod.GET)
#ResponseBody
public HttpEntity<byte[]> downloadPdf(#RequestParam int userId) {
byte[] result = service.generatePdf(userId);
HttpHeaders headers = new HttpHeaders();
if (result != null) {
headers.setContentType(new MediaType("application", "pdf"));
headers.set("Content-Disposition", "inline; filename=export.pdf");
headers.setContentLength(result.length);
return new HttpEntity(result, headers);
}
return new HttpEntity<>(header)
}
About exception handling for example you may throw YourCustomError and in controller annotated with #ControllerAdvice annotate a method with #ExceptionHandler(YourCustomError.class) and work with it.