Do
- Return ResponseEntity<Resource>from a handler method
- Specify Content-Type
- Set Content-Dispositionif necessary:
- filename
- type
- inlineto force preview in a browser
- attachmentto force a download
 
 
Example
@Controller
public class DownloadController {
    @GetMapping("/downloadPdf.pdf")
    // 1.
    public ResponseEntity<Resource> downloadPdf() {
        FileSystemResource resource = new FileSystemResource("/home/caco3/Downloads/JMC_Tutorial.pdf");
        // 2.
        MediaType mediaType = MediaTypeFactory
                .getMediaType(resource)
                .orElse(MediaType.APPLICATION_OCTET_STREAM);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(mediaType);
        // 3
        ContentDisposition disposition = ContentDisposition
                // 3.2
                .inline() // or .attachment()
                // 3.1
                .filename(resource.getFilename())
                .build();
        headers.setContentDisposition(disposition);
        return new ResponseEntity<>(resource, headers, HttpStatus.OK);
    }
}
Explanation
Return ResponseEntity<Resource>
When you return a ResponseEntity<Resource>, the ResourceHttpMessageConverter writes file contents
Examples of Resource implementations:
Specify Content-Type explicitly:
Reason: see "FileSystemResource is returned with content type json" question
Options:
- Hardcode the header
- Use the MediaTypeFactoryfrom Spring. TheMediaTypeFactorymapsResourcetoMediaTypeusing the/org/springframework/http/mime.typesfile
- Use a third party library like Apache Tika
Set Content-Disposition if necessary:
About Content-Disposition header:
The first parameter in the HTTP context is either inline (default value, indicating it can be displayed inside the Web page, or as the Web page) or attachment (indicating it should be downloaded; most browsers presenting a 'Save as' dialog, prefilled with the value of the filename parameters if present).
Use ContentDisposition in application:
- To preview a file in a browser: - ContentDisposition disposition = ContentDisposition
        .inline()
        .filename(resource.getFilename())
        .build();
 
- To force a download: - ContentDisposition disposition = ContentDisposition
        .attachment()
        .filename(resource.getFilename())
        .build();
 
Use InputStreamResource carefully:
Specify Content-Length using the HttpHeaders#setContentLength method if:
- The length is known
- You use InputStreamResource
Reason: Spring won't write Content-Length for InputStreamResource because Spring can't determine the length of the resource. Here is a snippet of code from ResourceHttpMessageConverter:
@Override
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
    // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
    // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
    if (InputStreamResource.class == resource.getClass()) {
        return null;
    }
    long contentLength = resource.contentLength();
    return (contentLength < 0 ? null : contentLength);
}
In other cases Spring sets the Content-Length:
~ $ curl -I localhost:8080/downloadPdf.pdf  | grep "Content-Length"
Content-Length: 7554270