2
votes

I am developing an API REST using Spring WebFlux, but I have problems when uploading files. They are stored but I don't get the expected return value.

This is what I do:

  1. Receive a Flux<Part>
  2. Cast Part to FilePart.
  3. Save parts with transferTo() (this return a Mono<Void>)
  4. Map the Mono<Void> to Mono<String>, using file name.
  5. Return Flux<String> to client.

I expect file name to be returned, but client gets an empty string.

Controller code

@PostMapping(value = "/muscles/{id}/image")
public Flux<String> updateImage(@PathVariable("id") String id, @RequestBody Flux<Part> file) {
    log.info("REST request to update image to Muscle");
    return storageService.saveFiles(file);
}

StorageService

public Flux<String> saveFiles(Flux<Part> parts) {
    log.info("StorageService.saveFiles({})", parts);
    return
            parts
            .filter(p -> p instanceof FilePart)
            .cast(FilePart.class)
            .flatMap(file -> saveFile(file));
}

private Mono<String> saveFile(FilePart filePart) {
    log.info("StorageService.saveFile({})", filePart);
    String filename = DigestUtils.sha256Hex(filePart.filename() + new Date());
    Path target = rootLocation.resolve(filename);
    try {
        Files.deleteIfExists(target);
        File file = Files.createFile(target).toFile();

        return filePart.transferTo(file)
                .map(r -> filename);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
2
Just as a side note: instead of .filter(p -> p instanceof FilePart) you could also use .filter(FilePart.class::isInstance) - Lino
Could you add several log operators in your pipeline and share the logs here? Maybe one after the cast operator to know how many parts you're getting, then another one after the flatMap to know how many files were transferred. - Brian Clozel
Thanks for advices. I solved it :) - Felipe Herrera

2 Answers

5
votes

FilePart.transferTo() returns Mono<Void>, which signals when the operation is done - this means the reactive Publisher will only publish an onComplete/onError signal and will never publish a value before that.

This means that the map operation was never executed, because it's only given elements published by the source.

You can return the name of the file and still chain reactive operators, like this:

return part.transferTo(file).thenReturn(part.filename());

It is forbidden to use the block operator within a reactive pipeline and it even throws an exception at runtime as of Reactor 3.2.

Using subscribe as an alternative is not good either, because subscribe will decouple the transferring process from your request processing, making those happen in different execution sequences. This means that your server could be done processing the request and close the HTTP connection while the other part is still trying to read the file part to copy it on disk. This is likely to fail in subtle ways at runtime.

1
votes

FilePart.transferTo() returns Mono<Void> that is a constant empty. Then, map after that was never executed. I solved it by doing this:

private Mono<String> saveFile(FilePart filePart) {
    log.info("StorageService.saveFile({})", filePart);
    String filename = DigestUtils.sha256Hex(filePart.filename() + new Date());
    Path target = rootLocation.resolve(filename);
    try {
        Files.deleteIfExists(target);
        File file = Files.createFile(target).toFile();
        return filePart
                .transferTo(file)
                .doOnSuccess(data -> log.info("do something..."))
                .thenReturn(filename);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}