Evgenia Rubanova
Evgenia Rubanova

Reputation: 53

Close CloseableHttpResponse after returning InputStream Spring MVC

My controller method returns InputStreamResource in response body. I'am getting InputStream from the CloseableHttpResponse in the TRY block and I close CloseableHttpResponse in FINALLY block.

The problem is that finally is called before the return and there is no chance to create InputStreamResource from the InputStream because CloseableHttpResponse is already closed.

What could be a possible solution in this case? Can I somehow close CloseableHttpResponse after returning InputStreamResource? I really need to use CloseableHttpClient and the method should return some stream.

    public ResponseEntity<InputStreamResource> getFileAsStream(@PathVariable("uuid") String uuid) throws IOException, InterruptedException {
        CloseableHttpResponse response = fileService.importFileByUuid(uuid);
        try {
            HttpEntity entity = response.getEntity();
            HttpHeaders responseHeaders = new HttpHeaders();
            responseHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength()));
            responseHeaders.add(HttpHeaders.CONTENT_TYPE, entity.getContentType().getValue());
            responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, response.getFirstHeader(HttpHeaders.CONTENT_DISPOSITION).getValue());

            if (entity != null) {
                InputStream inputStream = entity.getContent();
                InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
                return ResponseEntity.ok()
                            .headers(responseHeaders)
                            .body(inputStreamResource);
            }
            throw new IllegalStateException(String.format(
                    "Error during stream downloading, uuid = %s", uuid));
        } finally {
            response.close();
        }
    } 

Upvotes: 0

Views: 815

Answers (2)

HarryH
HarryH

Reputation: 13

Use the supplier for HTTP trailer headers in the servlet api to run the .close() side effect.

FileController.java

    @GetMapping(path = "/document")
    public ResponseEntity<?> getSingleDocument(
            @RequestParam(name = "docPath") String documentPath,
            HttpServletResponse response
    ) {
        return fileService.fetchPDF(documentPath, response);
    }

FileService.java

    record CloseableResponseEntity(ResponseEntity<?> response, Runnable onClose) {}

    public ResponseEntity<?> fetchPDF(String uri, HttpServletResponse response) {
        try {

            final CloseableResponseEntity responseHandler = getFileFromNetwork(uri);
            response.setTrailerFields(() -> {
                responseHandler.onClose().run();
                // can either return empty-map/null here.
                // both is fine.
                return null;
            });
            return responseHandler.response();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    private CloseableResponseEntity getFileFromNetwork(String uri) throws IOException {
        // Create Request
        HttpGet request = new HttpGet(uri);
        request.setHeader(HttpHeaders.AUTHORIZATION, fileSysAuthenticationKey);

        CloseableHttpResponse res = httpClient.execute(request);
        final HttpStatus status = HttpStatus.resolve(res.getCode());
        final String contentType = res.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue();
        final String contentLength = res.getFirstHeader(HttpHeaders.CONTENT_LENGTH).getValue();
        // Error handling -- read content
        if (!Objects.nonNull(status) || status.isError()) {
            res.close();
            throw new DataRetrievalException("PDF cannot be retrieved" + status);
        }
        // On Success -- don't consume the stream and let Spring handle.
        final InputStream pdfSocket = res.getEntity().getContent();
        return new CloseableResponseEntity(
                ResponseEntity.ok()
                        .header(HttpHeaders.CONTENT_TYPE, contentType)
                        .body(new InputStreamResource(pdfSocket) {
                            @Override
                            public long contentLength() throws IOException {
                                return Long.parseLong(contentLength);
                            }
                        }),
                () -> {
// To check whether the stream is actually fully read at this point.
// only res.close() is needed.
                    try {
                        res.close();
                        logger.info("close httpresponse here, isstreaming: {}",
                                res.getEntity().isStreaming());
                        logger.info("{}", EntityUtils.toString(res.getEntity()));
                        res.close();
                    } catch (IOException ignored) {
                        ignored.printStackTrace();
                    }
                }
        );
    }

Upvotes: 0

Karim
Karim

Reputation: 1034

You can always create a copy of the InputStream obtained from the HttpEntity and use the copy to create the InputStreamResource that is returned in the ResponseEntity. This way, the original InputStream obtained from the HttpEntity can be safely closed before the ResponseEntity is returned.

InputStream inputStream = entity.getContent();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(inputStream, outputStream);
byte[] contentBytes = outputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(contentBytes);
InputStreamResource inputStreamResource = new InputStreamResource(byteArrayInputStream);

Or save use the byte[] and use it to return ByteArrayResource instead of InputStreamResource

Upvotes: 0

Related Questions