keyzj
keyzj

Reputation: 341

How to stream file from Multipart/form-data in Spring WebFlux

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:

  1. I don't want to read the whole file into memory;
  2. Array size in limited to Integer.MAX_VALUE;
  3. Array encodes as base64 String, which takes extra memory;
  4. 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 ...;
}

Upvotes: 3

Views: 11943

Answers (2)

mflame
mflame

Reputation: 157

I have same issue with filename here, however as we are developing with Spring WebFlux our FilePart is wrapped in Flux

@RequestMapping(
  method = RequestMethod.POST,
  value = "/api/endpoint"
  produces = { "application/json" },
  consumes = { "multipart/form-data" }
)
public <Mono<ResponseBody<Entity>> upload(@RequestPart("file") Flux<FilePart> file, ServerWebExchange exchange) {
  Flux<DataBuffer> dataBufferFlux = file.flatMap(Part::content);
  String filename = // TODO how to get it?
  MultipartBodyBuilder builder = new MultipartBodyBuilder();
  builder.asyncPart("file", dataBufferFlux, DataBuffer.class)
         .filename(filename);
  ... 
}

Now the issue is how to set filename at MultiPartBodyBuilder properly.

Upvotes: 0

keyzj
keyzj

Reputation: 341

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());

Upvotes: 1

Related Questions