Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Web Reactive client

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 [email protected]

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?

like image 280
Ozzie Avatar asked Nov 09 '17 12:11

Ozzie


People also ask

What is spring reactive WebClient?

In simple words, the Spring WebClient is a component that is used to make HTTP calls to other services. It is part of Spring's web reactive framework, helps building reactive and non-blocking applications. To make HTTP requests, you might have used Spring Rest Template, which was simple and always blocking web client.

What is reactive web client?

WebClient is a non-blocking, reactive client for performing HTTP requests with Reactive Streams back pressure. WebClient provides a functional API that takes advantage of Java 8 Lambdas. By default, WebClient uses Reactor Netty as the HTTP client library. But others can be plugged in through a custom.

What is spring web client?

What Is Spring WebClient? The Spring WebClient is a reactive HTTP library; it's the follow-up to the Spring RestTemplate which is now in maintenance mode. Also, whereas the RestTemplate was a synchronous blocking library, WebClient is an asynchronous non-blocking library.

Can I use WebClient in spring MVC?

It can take time to get used to Reactive APIs, but the WebClient has interesting features and can also be used in traditional Spring MVC applications. You can use WebClient to communicate with non-reactive, blocking services, too.


4 Answers

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);
like image 99
Ozzie Avatar answered Sep 22 '22 10:09

Ozzie


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());
like image 23
Manh Tai Avatar answered Sep 22 '22 10:09

Manh Tai


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()))
like image 23
V.Aggarwal Avatar answered Sep 22 '22 10:09

V.Aggarwal


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");
like image 24
Brian Clozel Avatar answered Sep 20 '22 10:09

Brian Clozel