How to return application/pdf through Mono in a Reactive way

Viewed 991

I am currently using Spring WebFlux to try build an async end-point, which fetches a PDF from a third-party end-point via Web Client before returning the PDF back to our API consumer. However, I am struggling with returning a Mono<ResponseEntity> with content type application/pdf due to the below exception:

Resolved [org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class reactor.core.publisher.MonoMapFuseable] with preset Content-Type 'application/pdf']

Here is controller implementation. My question is:

  • Is my implementation in the right direction, or would I need to create some sort of converter?
  • Does Mono<ResponseEntity> even support returning a PDF as a response body?
    @RequestMapping(value="/get-pdf", method = RequestMethod.GET)
    public Mono<ResponseEntity> getPDFAsync() {

        String url = "http://some-end-point";
        WebClient client = WebClient.create(url);

        return client.get()
                .accept(MediaType.APPLICATION_PDF)
                .exchangeToMono(response ->
                        Mono.just(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
                            .body(response.bodyToMono(ByteArrayResource.class)
                                    .map(byteArrayResource -> byteArrayResource.getByteArray())
                        )));
    }
2 Answers

To download a file reactively, you could supply the file as a Flux<DataBuffer>, where DataBuffer is org.springframework.core.io.buffer.DataBuffer, like this:

    // some shared buffer factory.
    private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);



    @RequestMapping(value = "/download",
            method = RequestMethod.GET,
            produces = {MediaType.APPLICATION_PDF_VALUE}
    )
    public Mono<ResponseEntity<Flux<DataBuffer>>> downloadDocument(
            ...
    ) {
        return Mono.fromCallable(() -> {
           return ResponseEntity.ok(
             DataBufferUtils.read(
               new File("somepdf.pdf").toPath(), 
               dataBufferFactory, 
               8096
           ))
        });
    }

Or more specifically, since you seem to be using the WebFlux WebClient, you can forward the response body flux directly to your own response, without having to buffer the complete response first:

    @RequestMapping(value = "/download",
            method = RequestMethod.GET,
            produces = {MediaType.APPLICATION_PDF_VALUE}
    )
    public Mono<ResponseEntity<Flux<DataBuffer>>> downloadDocument(
            ...
    ) {
          String url = "http://some-end-point";
          WebClient client = WebClient.create(url);

          return client.get()
                  .accept(MediaType.APPLICATION_PDF)
                  .exchange()
                  .map(response -> response.bodyToFlux(DataBuffer.class))
                  .map(ResponseEntity::ok);
    }

Hint: I hope you are reusing the WebClient instance and not instantiating a new one on each request.

I have found the answer! In short, returning Mono<byte[]>, and add produces = {MediaType.APPLICATION_PDF_VALUE} to @RequestMapping works. See example below.

    @RequestMapping(value="/get-pdf",  produces = {MediaType.APPLICATION_PDF_VALUE},  method = RequestMethod.GET)
public Mono<byte[]> getPdf() {
    
    String url = "some-end-point";
    WebClient client = WebClient.create(url);

    return client.get()
            .accept(MediaType.APPLICATION_PDF)
            .exchangeToMono(response -> response
                    .bodyToMono(ByteArrayResource.class))
            .map(byteArrayResource -> byteArrayResource.getByteArray());
}
Related