8

I have the following endpoint code to serve PDF files.

@RequestMapping
ResponseEntity<byte[]> getPDF() {
  File file = ...;
  byte[] contents = null;
  try {
    try (FileInputStream fis = new FileInputStream(file)) {
      contents = new byte[(int) file.length()];
      fis.read(contents);
    }
  } catch(Exception e) {
    // error handling
  }
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  headeres.setCacheControl("must-revalidate, post-check=0, pre-check=0");
  return new ResponseEntity<>(contents, headers, HttpStatus.OK);
}

How can I convert above into a reactive type Flux/Mono and DataBuffer.

I have check DataBufferUtils but It doesn't seem to offer what I needed. I didn't find any example either.

Bk Santiago
  • 1,523
  • 3
  • 13
  • 24

3 Answers3

16

The easiest way to achieve that would be with a Resource.

@GetMapping(path = "/pdf", produces = "application/pdf")
ResponseEntity<Resource> getPDF() {
  Resource pdfFile = ...;
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  return ResponseEntity
    .ok().cacheControl(CacheControl.noCache())
    .headers(headers).body(resource);
}

Note that DataBufferUtils has some useful methods there that convert an InputStream to a Flux<DataBuffer>, like DataBufferUtils#read(). But dealing with a Resource is still superior.

Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
2

Below is the code to return the attachment as byte stream:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Mono<byte[]> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMap(response -> response.toEntity(byte[].class));
}

This approach is very simple but the disadvantage is it will the load the entire attachment into memory. If the file size is larger, then it will be a problem.

To overcome we can use DataBuffer which will send the data in chunks. This is an efficient solution and it will work for any large size file. Below is the modified code using DataBuffer:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Flux<DataBuffer> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMapMany(response -> response.toEntity(DataBuffer.class));
}

In this way, we can send attachments in a reactive fashion.

Sudharsan Thumatti
  • 2,145
  • 1
  • 10
  • 6
  • f I'm correct, the whole body is loaded into memory what can be an issue on for really big files – Krzysztof Skrzynecki May 28 '20 at 13:42
  • @KrzysztofSkrzynecki Yes.. The optimized way to send the data is chunks to the client.. – Sudharsan Thumatti May 30 '20 at 03:36
  • I get reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response when using the second approach – abitcode Aug 22 '20 at 09:12
  • @KrzysztofSkrzynecki Entire image won't be loaded into the memory. Using reactive, we are loading only one chunk at a time and returning back to the client. – Sudharsan Thumatti Oct 21 '21 at 06:04
  • @abitcode Please try to increase the timeout for webclient builder.. – Sudharsan Thumatti Oct 21 '21 at 06:05
  • @SudharsanThumatti long time since I have tested it (May 2020) but after another review I'm pretty sure that because `response.toEntity(DataBuffer.class)` creates Mono object, whole response needs to be loaded into memory before it is sent further. `.bodyToFlux( DataBuffer.class )` does splitting into chunk for you – Krzysztof Skrzynecki Oct 21 '21 at 12:26
0

Same Problem with me.

I use Webflux Spring WebClient

I write style RouterFunction

My solution below,

ETaxServiceClient.java

final WebClient defaultWebClient;


public Mono<byte[]> eTaxPdf(String id) {
    return defaultWebClient
            .get()
            .uri("-- URL PDF File --")
            .accept(MediaType.APPLICATION_OCTET_STREAM)
            .exchange()
            .log("eTaxPdf -> call other service")
            .flatMap(response -> response.toEntity(byte[].class))
            .flatMap(responseEntity -> Mono.just(Objects.requireNonNull(responseEntity.getBody())));
}

ETaxHandle.java

@NotNull
public Mono<ServerResponse> eTaxPdf(ServerRequest sr) {
    Consumer<HttpHeaders> headers = httpHeaders -> {
        httpHeaders.setCacheControl(CacheControl.noCache());
        httpHeaders.setContentDisposition(
                ContentDisposition.builder("inline")
                        .filename(sr.pathVariable("id") + ".pdf")
                        .build()
        );
    };
    return successPDF(eTaxServiceClient
            .eTaxPdf(sr.pathVariable("id"))
            .switchIfEmpty(Mono.empty()), headers);
}

ETaxRouter.java

@Bean
public RouterFunction<ServerResponse> routerFunctionV1(ETaxHandle handler) {
    return route()
            .path("/api/v1/e-tax-invoices", builder -> builder
                    .GET("/{id}", handler::eTaxPdf)
            )
            .build();
}

CommonHandler.java

Mono<ServerResponse> successPDF(Mono<?> mono, Consumer<HttpHeaders> headers) {
    return ServerResponse.ok()
            .headers(headers)
            .contentType(APPLICATION_PDF)
            .body(mono.map(m -> m)
                    .subscribeOn(Schedulers.elastic()), byte[].class);
}

Result: Successfully displayed on the browser.

Work for me.

enter image description here

bamossza
  • 3,676
  • 1
  • 29
  • 28