8

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?

Ozzie
  • 11,613
  • 4
  • 21
  • 24

4 Answers4

8

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);
Ozzie
  • 11,613
  • 4
  • 21
  • 24
  • 4
    Dont forget the call to `map.set("multipartFile", resource)` – A. Masson Oct 22 '19 at 16:15
  • Must be simpler according to https://github.com/spring-projects/spring-framework/issues/23372#issuecomment-516793172 – aholub7x Oct 22 '19 at 17:43
  • This is definitely should be marked as a solution. The official documentation https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client-body doesn't mention this. Also, a simple way mentioned by aholub7x works great. – BarbosSergos Feb 19 '20 at 08:08
3

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());
Lari Hotari
  • 5,190
  • 1
  • 36
  • 43
Manh Tai
  • 356
  • 4
  • 8
3

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()))
V.Aggarwal
  • 557
  • 4
  • 12
1

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");
Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • That was only for the code sample here. In the actual code the resource already is a bytearray from a content repository. Not a file from the filesystem. – Ozzie Nov 13 '17 at 10:45