javahttpioquarkus

how to avoid oom when direct return InputStream using quarkus


First, I obtained the target file to be downloaded through Minio like this.

public GetObjectResponse getFile(String bucket, String object) {
        validateEnableMinio();
        try {
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(object)
                    .build();
            return minioClient.getObject(getObjectArgs);
        } catch (Exception e) {
            LOGGER.error("minio exception", e);
            throw new RuntimeException("get file error");
        }
    }

GetObjectResponse is extends FilterInputStream. So I thought of using the Response class to return a BufferedInputStream class to avoid load all bytes in memory.

 @GET
    @Path("/download")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Uni<Response> download1(@RestQuery String fileName) {
        var stat = minioService.statObject(fileName);
        try (GetObjectResponse getObjectResponse =  minioService.getFile(fileName)) {
            BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectResponse);
            Response.ResponseBuilder builder = Response.ok(bufferedInputStream);
            builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
            builder.header("Content-Disposition", "attachment; filename=" + stat.object());
            return Uni.createFrom().item(builder.build());
        } catch (Exception e) {
            LOGGER.error("error", e);
            return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()) ;
        }
    }

but i download a empty file with 0 bytes.

enter image description here

i can download full file with code like this: but use readAllBytes() method

@GET
    @Path("/download")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Uni<Response> download1(@RestQuery String fileName) {
        var stat = minioService.statObject(fileName);
        try (GetObjectResponse getObjectResponse =  minioService.getFile(fileName)) {
            BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectResponse);
            Response.ResponseBuilder builder = Response.ok(bufferedInputStream.readAllBytes());
            builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
            builder.header("Content-Disposition", "attachment; filename=" + stat.object());
            return Uni.createFrom().item(builder.build());
        } catch (Exception e) {
            LOGGER.error("error", e);
            return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()) ;
        }
    }

So, I changed my approach and tried to download a local file directly instead of the file stream returned through HTTP.

@GET
    @Path("/download1")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Uni<Response> downloadFile() {
        String filePath = "D:\\xxx\\settings.xml"; // 替换为实际文件路径

        File file = new File(filePath);

        if (!file.exists() || !file.isFile()) {
            return Uni.createFrom().item(Response.status(Response.Status.NOT_FOUND).build());
        }

        try {
            InputStream is = new BufferedInputStream(new FileInputStream(file));

            return Uni.createFrom().item(Response.ok(is)
                    .header("Content-Disposition", "attachment; filename=" + file.getName())
                    .build());
        } catch (IOException e) {
            return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("error").build());
        }
    }

It works perfectly.

The first solution that came to my mind was to write the InputStream returned by HTTP to a local file, and then return the stream of the local file.

However, how can I directly return the InputStream to the front-end without using file transfer as an intermediary?

follow-up

I try return Response.ok with inputstream,but it still not work.it still return 0 bytes file

@GET
    @Path("/download")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response download1(@RestQuery String fileName) {
        var stat = minioService.statObject(fileName);
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket("test1")
                .object(fileName)
                .build();
        try (InputStream inputStream =  minioClient.getObject(getObjectArgs)) {
            Response.ResponseBuilder builder = Response.ok(inputStream);
            builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
            builder.header("Content-Disposition", "attachment; filename=" + stat.object());
            return builder.build();
        } catch (Exception e) {
            LOGGER.error("exception", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }
    }

Solution

  • Do something like this:

    @GET
    @Path("/download")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response download1(@RestQuery String fileName, @Context Closer closer) {
        var stat = minioService.statObject(fileName);
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket("test1")
                .object(fileName)
                .build();
    
        InputStream inputStream;
        try {
            inputStream =  minioClient.getObject(getObjectArgs);
            closer.add(inputStream);
            Response.ResponseBuilder builder = Response.ok(inputStream);
            builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
            builder.header("Content-Disposition", "attachment; filename=" + stat.object());
            return builder.build();
        } catch (Exception e) {
            if (inputStream != null) {
               try {
                  inputStream.close();
               } catch (Exception ignored) {}
            }
            LOGGER.error("exception", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }
    }
    

    The reason is that you don't want to close the InputStream in the JAX-RS method