How to Handle File Response in Spring Boot REST APIs

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