148

I am trying to download a file from a Spring boot rest service.

@RequestMapping(path="/downloadFile",method=RequestMethod.GET)
    @Consumes(MediaType.APPLICATION_JSON_VALUE)
    public  ResponseEntity<InputStreamReader> downloadDocument(
                String acquistionId,
                String fileType,
                Integer expressVfId) throws IOException {
        File file2Upload = new File("C:\\Users\\admin\\Desktop\\bkp\\1.rtf");
        HttpHeaders headers = new HttpHeaders();
        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
        headers.add("Pragma", "no-cache");
        headers.add("Expires", "0");
        InputStreamReader i = new InputStreamReader(new FileInputStream(file2Upload));
        System.out.println("The length of the file is : "+file2Upload.length());

        return ResponseEntity.ok().headers(headers).contentLength(file2Upload.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(i);
        }

When I tried to download the file from the browser, it starts the download, but always fails. Is there anything wrong with the service which is causing the download to fail?

lambda
  • 3,295
  • 1
  • 26
  • 32
kiran
  • 2,280
  • 2
  • 23
  • 30

7 Answers7

254

Option 1 using an InputStreamResource

Resource implementation for a given InputStream.

Should only be used if no other specific Resource implementation is > applicable. In particular, prefer ByteArrayResource or any of the file-based Resource implementations where possible.

@RequestMapping(path = "/download", method = RequestMethod.GET)
public ResponseEntity<Resource> download(String param) throws IOException {

    // ...

    InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

    return ResponseEntity.ok()
            .headers(headers)
            .contentLength(file.length())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}

Option2 as the documentation of the InputStreamResource suggests - using a ByteArrayResource:

@RequestMapping(path = "/download", method = RequestMethod.GET)
public ResponseEntity<Resource> download(String param) throws IOException {

    // ...

    Path path = Paths.get(file.getAbsolutePath());
    ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

    return ResponseEntity.ok()
            .headers(headers)
            .contentLength(file.length())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}
yelliver
  • 5,648
  • 5
  • 34
  • 65
fateddy
  • 6,887
  • 3
  • 22
  • 26
  • 4
    I am trying to do it for word document .doc format, but while downloading the format is gone and file is downloaded without file extension and the file name is response while downloading. Any suggestion? – Tulsi Jain Jan 10 '18 at 10:42
  • 54
    @TulsiJain add the Content-Disposition HttpHeader: `HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=myDoc.docx");` – fateddy Jan 10 '18 at 14:16
  • 9
    just in case you're unlucky enough to be using plain Spring instead of Spring Boot, you need to make sure that an instance of `ResourceHttpMessageConverter` is added to your list of HttpMessageConverters. Create a `@Configuration` class that extends `WebMvcConfigurerAdapter`, implement the configureMessageConverters() method and add a `converters.add(new ResourceHttpMessageConverter());` line – ashario May 17 '18 at 03:57
  • 6
    Questions: Option 1 does not seem to close the stream. Where is the magic? Option 2 seems to load the complete file into memory before sending. Correct? Alternatives? THX! – eventhorizon Aug 08 '19 at 19:17
  • 1
    @eventhorizon In Option 1, Spring Boot will close the stream. See this: https://stackoverflow.com/a/48660203/7363182 – dems98 Mar 01 '21 at 12:19
  • Is there anyway to notify the client of an error in the middle of the streaming/zipping process ? It seems entering in the catch block does NOT effect the response to indicate anything other than closing the resources since the response already committed ! – Anddo Oct 17 '21 at 11:48
  • 3
    For huge files ByteArrayResource will work? It will not take entire heap space? Or should we go for StreamingResponseBody so that Out Of Memory will not occur? – jagga Feb 24 '22 at 09:57
  • hello @fateddy , i used `httpHeaders.add(CONTENT_DISPOSITION, "attachment;File-Name="+filename);` and when returning i used `ResponseEntity.ok().contentType(MediaType.parseMediaType(Files.probeContentType(filePath))).headers(httpHeaders).body(resource)` but it still throwing `Invalid mime type "null": 'mimeType' must not be empty` – Rajanboy Sep 05 '22 at 11:45
  • shouldn't it be InputStream inputStream = resource.getInputStream(); for option 1? Original option 1 wont work in executable jar.? – mattsmith5 Oct 03 '22 at 16:37
  • I have a question; How can I download more then one file? I mean, I have 10 pdf files and I want to download all of them in only one request? Is there a way? – Semih Erkaraca Dec 13 '22 at 10:53
  • how do you escape the name of the file? in my case spaces and ( ) causes errors – Alfredo Morales Aug 12 '23 at 00:12
61

The below Sample code worked for me and might help someone.

import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@RestController
@RequestMapping("/app")
public class ImageResource {

    private static final String EXTENSION = ".jpg";
    private static final String SERVER_LOCATION = "/server/images";

    @RequestMapping(path = "/download", method = RequestMethod.GET)
    public ResponseEntity<Resource> download(@RequestParam("image") String image) throws IOException {
        File file = new File(SERVER_LOCATION + File.separator + image + EXTENSION);

        HttpHeaders header = new HttpHeaders();
        header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=img.jpg");
        header.add("Cache-Control", "no-cache, no-store, must-revalidate");
        header.add("Pragma", "no-cache");
        header.add("Expires", "0");

        Path path = Paths.get(file.getAbsolutePath());
        ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

        return ResponseEntity.ok()
                .headers(header)
                .contentLength(file.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(resource);
    }

}
Rajesh
  • 4,273
  • 1
  • 32
  • 33
27

I would suggest using a StreamingResponseBody since with it, the application can write directly to the response (OutputStream), without holding up the Servlet container thread. It is a good approach if you are downloading a very large file.

@GetMapping("download")
public StreamingResponseBody downloadFile(HttpServletResponse response, @PathVariable Long fileId) {

    FileInfo fileInfo = fileService.findFileInfo(fileId);
    response.setContentType(fileInfo.getContentType());
    response.setHeader(
        HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fileInfo.getFilename() + "\"");

    return outputStream -> {
        int bytesRead;
        byte[] buffer = new byte[BUFFER_SIZE];
        InputStream inputStream = fileInfo.getInputStream();
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
    };
}

Ps.: When using StreamingResponseBody, it is highly recommended to configure TaskExecutor used in Spring MVC for executing asynchronous requests. TaskExecutor is an interface that abstracts the execution of a Runnable.

More info: https://medium.com/swlh/streaming-data-with-spring-boot-restful-web-service-87522511c071

Felipe Desiderati
  • 2,414
  • 3
  • 24
  • 42
  • https://stackoverflow.com/users/8650621/felipe-desiderati don't we need to close the inputStream ? – jagga Feb 24 '22 at 09:29
  • 2
    No, because it’s auto closeable. Just check that we are returning a lambda, and it will be called after by the Framework. – Felipe Desiderati Feb 24 '22 at 14:48
  • https://stackoverflow.com/users/8650621/felipe-desiderati can we use ByteArrayResource if the files are max 10 MB and stored in DB as byte array? or StreamingResponseBody is better option? – jagga Feb 24 '22 at 15:42
13

I want to share a simple approach for downloading files with JavaScript (ES6), React and a Spring Boot backend:

  1. Spring boot Rest Controller

Resource from org.springframework.core.io.Resource

    @SneakyThrows
    @GetMapping("/files/{filename:.+}/{extraVariable}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename, @PathVariable String extraVariable) {

        Resource file = storageService.loadAsResource(filename, extraVariable);
        return ResponseEntity.ok()
               .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
               .body(file);
    }
  1. React, API call using AXIOS

Set the responseType to arraybuffer to specify the type of data contained in the response.

export const DownloadFile = (filename, extraVariable) => {
let url = 'http://localhost:8080/files/' + filename + '/' + extraVariable;
return axios.get(url, { responseType: 'arraybuffer' }).then((response) => {
    return response;
})};

Final step > downloading
with the help of js-file-download you can trigger browser to save data to file as if it was downloaded.

DownloadFile('filename.extension', 'extraVariable').then(
(response) => {
    fileDownload(response.data, filename);
}
, (error) => {
    // ERROR 
});
fetahokey
  • 185
  • 2
  • 16
  • 1
    I ran across this issue and was curious about the enclosure of the CONTENT_DISPOSITION header's filename in double quotes. Turns out that if you have a file with spaces in its name, you won't get the entire filename in the response without the double quotes. Good call, @fetahokey – Dana Apr 21 '21 at 15:01
10

If you need to download a huge file from the server's file system, then ByteArrayResource can take all Java heap space. In that case, you can use FileSystemResource

Taras Melon
  • 377
  • 4
  • 16
  • 1
    can we use file system resource if the file is stored in byte array in DB? and on click we need to to download that file. Also what is the definition of huge file . my files are 255213 bytes – jagga Feb 24 '22 at 06:41
  • @jagga I think that a better solution is using ByteArrayResource (see Option 2 here https://stackoverflow.com/a/35683261/4141492), huge file for example more than 500MB – Taras Melon Feb 24 '22 at 14:55
4
    @GetMapping("/downloadfile/{productId}/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable(value = "productId") String productId,
        @PathVariable String fileName, HttpServletRequest request) {
    // Load file as Resource
    Resource resource;

    String fileBasePath = "C:\\Users\\v_fzhang\\mobileid\\src\\main\\resources\\data\\Filesdown\\" + productId
            + "\\";
    Path path = Paths.get(fileBasePath + fileName);
    try {
        resource = new UrlResource(path.toUri());
    } catch (MalformedURLException e) {
        e.printStackTrace();
        return null;
    }

    // Try to determine file's content type
    String contentType = null;
    try {
        contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
    } catch (IOException ex) {
        System.out.println("Could not determine file type.");
    }

    // Fallback to the default content type if type could not be determined
    if (contentType == null) {
        contentType = "application/octet-stream";
    }

    return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
            .body(resource);
}

To test it, use postman

http://localhost:8080/api/downloadfile/GDD/1.zip

Feng Zhang
  • 1,698
  • 1
  • 17
  • 20
0

using Apache IO could be another option for copy the Stream

@RequestMapping(path = "/file/{fileId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> downloadFile(@PathVariable(value="fileId") String fileId,HttpServletResponse response) throws Exception {

    InputStream yourInputStream = ...
    IOUtils.copy(yourInputStream, response.getOutputStream());
    response.flushBuffer();
    return ResponseEntity.ok().build();
}

maven dependency

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.3.2</version>
    </dependency>
JPG
  • 750
  • 2
  • 8
  • 23
  • Send back directly InputStreamResource with inputStream. You don't need to copy stream. – Steph Jan 22 '21 at 17:04