javaspring-mvcoctet-stream

How to create an octet-stream endpoint for large files in Spring MVC?


Please assist me with finding the correct way to use Spring MVC to implement an endpoint that downloads large files.

I am aware of the approach involving explicit writing into the HttpServletResponse output stream:

@PostMapping("/download")
void downloadFile(
     @PathVariable("path") String path,
     HttpServletResponse response
) throws Exception {
    try (var output = response.getOutputStream()) {
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        service.readFilePart(path, output));
    }
}

However, one disadvantage is that when the service.readFilePart method throws an exception, the endpoint still returns HTTP 200 instead of a response with an error status and some JSON.

I found several approaches, but they all suggest collecting all file data in memory before sending the response, such as:

try (var output = new ByteArrayOutputStream()) {
    service.readFilePart(path, output));
    return ResponseEntity.ok()
                  .contentType(MediaType.APPLICATION_OCTET_STREAM)
                  .body(output.toByteArray());
}

I am seeking a solution that can handle large files without loading chunks larger than 1MB into memory, while also making use of standard Spring MVC features (e.g. exception handlers).

And it seems that using the HttpServletResponse parameter might be breaking a level of abstraction. Is it possible to achieve similar functionality using higher-level abstractions in Spring MVC?


Solution

  • 
    @GetMapping("/download")
    public ResponseEntity<StreamingResponseBody> downloadFile(@RequestParam("path") String path) {
        try {
            // Assuming you have a method to get the file size
            long fileSize = fileService.getFileSize(path);
    
            StreamingResponseBody stream = outputStream -> {
                try {
                    fileService.readFilePart(path, outputStream);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to write file", e);
                }
            };
    
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + extractFilename(path) + "\"")
                    .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize))  // Set the content length
                    .body(stream);
    
        } catch (Exception e) {
            // Log the exception and return an appropriate error response
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(null);  // You can customize the response as needed
        }
    }