How to Handle File Response in Spring Boot REST APIs

In this article we’ll dive into three different ways for returning files in a Spring Boot application. Every approach has different trade-offs in terms of memory usage, performance, and complexity. We’ll provide guidance on selecting the best option for your use case.

We’ll look into generating a csv file that will be downloaded automatically when the endpoint is called (Content-Disposition: attachment)

Option 1: byte[]

@GetMapping("/download")
public ResponseEntity<byte[]> download() {
    String content = "csv here";

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    headers.setContentDisposition(ContentDisposition.attachment().filename("filename.csv").build());

    return ResponseEntity.ok()
            .headers(headers)
            .body(content.getBytes(StandardCharsets.UTF_8));
}

Pros:

  • Straightforward implementation

Cons:

  • High memory usage, as the entire file is loaded into memory
  • Performance issues with large files

Option 2: InputStreamResource

@GetMapping("/download")
public ResponseEntity<InputStreamResource> download() {
    String content = "csv here";
    ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    headers.setContentDisposition(ContentDisposition.attachment().filename("filename.csv").build());

    return ResponseEntity.ok()
            .headers(headers)
            .body(new InputStreamResource(inputStream));
}

Pros:

  • Memory efficient, as it streams data rather than loading everything at once
  • More scalable than byte[] for larger files

Cons:

  • More complex than returning a byte array, you need to handle resource management and ensure the InputStream is closed

Option 3: Using StreamingResponseBody

@GetMapping("/download")
public ResponseEntity<StreamingResponseBody> download() {
    String content = "csv here";
    StreamingResponseBody stream = outputStream -> {
        try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
            writer.println(content);
            writer.flush();
        }
    };

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    headers.setContentDisposition(ContentDisposition.attachment().filename("filename.csv").build());

    return ResponseEntity.ok()
            .headers(headers)
            .body(stream);
}

It is highly suggested to configure a TaskExecutor for handling async requests and and avoid thread exhaust

@EnableAsync
@EnableScheduling
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Bean(name = "taskExecutor")
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(core pool size here);
        executor.setMaxPoolSize(max pool size here);
        executor.setQueueCapacity(queue capacity here);
        return executor;
    }
}

Pros:

  • Less memory usage, it streams the response directly to the client
  • Best for handling very large files

Cons:

  • More complex error handling, it’s more difficult to set a custom status code if an error occurs while streaming
  • More complex implementation

Conclusion

We had a look at different approaches for sending a CSV file response with Spring Boot. Considering pros and cons for each option, we recommend using StreamingReponseBody for large files, InputStreamResource for medium files and byte[] for small files