Enterprise Java

Multipart Data Streaming with Spring MVC and WebFlux

Efficient handling of large file uploads and downloads is a common requirement in modern web applications. Traditional approaches often buffer entire files in memory or on disk, which can lead to excessive resource usage and performance bottlenecks when working with large payloads. To overcome these limitations, Spring provides mechanisms for streaming multipart data sequentially.

In this article, we explore how to implement streaming for multipart uploads and downloads in both Spring MVC and Spring WebFlux (Reactive-based) applications.

1. Spring MVC – Sequential Streaming

This section shows how to implement streaming multipart uploads and streaming multipart responses sequentially using Spring MVC.

Configuring Multipart Handling in Spring MVC

First, we need to configure multipart handling in application.properties. By default, Spring Boot may buffer multipart uploads in memory before writing them to disk, depending on the file size. This can cause memory pressure when handling large files. To ensure that files are streamed sequentially without being buffered, we can set the file-size threshold to zero and define maximum limits for uploads.

# Ensure multipart files are always written directly to disk
spring.servlet.multipart.file-size-threshold=0

# Define maximum file size allowed per upload
spring.servlet.multipart.max-file-size=10MB

# Define maximum request size (sum of all files + form data in a request)
spring.servlet.multipart.max-request-size=20MB

The property spring.servlet.multipart.file-size-threshold=0 ensures that uploaded files are written directly to disk instead of being buffered in memory. This is especially important for sequential streaming, as it prevents large files from consuming excessive heap space. By streaming data directly from the request input stream to the destination file, the application achieves efficient and resource-friendly file uploads.

In addition, spring.servlet.multipart.max-file-size=10MB defines the maximum size allowed for a single uploaded file, while spring.servlet.multipart.max-request-size=20MB sets the upper limit for the total request payload, including multiple files and form data. These limits protect the server from handling files that are too large, helping maintain stability and preventing resource exhaustion during uploads.

1.1 Controller: Streaming Upload (MVC)

In a traditional Spring MVC application, file uploads using MultipartFile are often buffered entirely in memory or on disk before the application processes them. For smaller files, this may be acceptable, but for larger payloads, it can become inefficient. To address this, we can stream the uploaded file directly from the request input stream to the server’s file system.

@Controller
public class MvcStreamingUploadController {

    private static final Logger log = LoggerFactory.getLogger(MvcStreamingUploadController.class);
    private final Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");

    @PostMapping("/upload")
    public ResponseEntity<String> streamFileUpload(@RequestPart("file") MultipartFile file) throws IOException {
        Files.createDirectories(uploadRoot);
        Path targetPath = uploadRoot.resolve(System.currentTimeMillis() + "-" + file.getOriginalFilename());

        try (InputStream inputStream = file.getInputStream(); OutputStream outputStream = Files.newOutputStream(targetPath)) {
            inputStream.transferTo(outputStream);
        }

        log.info("File [{}] streamed successfully to {}", file.getOriginalFilename(), targetPath);
        return ResponseEntity.ok("Upload successful: " + file.getOriginalFilename());
    }
}

The /upload endpoint accepts a multipart file parameter named "file", creates the target directory (mvc-uploads) if it doesn’t exist, and streams the file content directly from the input stream to a file on disk using inputStream.transferTo(outputStream). This approach avoids buffering large files in memory, improving efficiency and reducing resource usage.

1.2 Controller: Streaming Multipart Download (MVC)

To stream a multipart response sequentially, where the server pushes multiple parts one after another, you can write the multipart MIME structure directly to the HttpServletResponse using StreamingResponseBody. By defining a dynamic boundary and setting the content type to multipart/mixed, each part can be delivered efficiently in sequence without loading all files or data into memory at once, making it ideal for sending multiple files or mixed content to a client.

@Controller
public class MvcStreamingDownloadController {

    private static final Logger log = LoggerFactory.getLogger(MvcStreamingDownloadController.class);
    private final Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");
    private static final String BOUNDARY = "MvcBoundary_" + System.currentTimeMillis();

    @GetMapping("/download-multipart")
    public StreamingResponseBody downloadMultipart(HttpServletResponse response) throws IOException {
        response.setContentType("multipart/mixed; boundary=" + BOUNDARY);

        return outputStream -> {
            try (BufferedOutputStream bos = new BufferedOutputStream(outputStream);
                 OutputStreamWriter writer = new OutputStreamWriter(bos)) {

                // List of two example files to stream
                Path file1 = uploadRoot.resolve("example-file1.txt");
                Path file2 = uploadRoot.resolve("example-file2.txt");
                Path[] filesToDownload = {file1, file2};

                for (Path filePath : filesToDownload) {
                    if (!Files.exists(filePath)) continue; // skip missing files

                    // Write multipart boundary and headers
                    writer.write("--" + BOUNDARY + "\r\n");
                    writer.write("Content-Disposition: attachment; filename=\"" + filePath.getFileName() + "\"\r\n");
                    writer.write("Content-Type: text/plain\r\n\r\n");
                    writer.flush();

                    // Stream file content line by line
                    try (BufferedReader reader = Files.newBufferedReader(filePath)) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            writer.write(line);
                            writer.write(System.lineSeparator());
                        }
                    }
                    writer.write("\r\n"); // separate parts
                    writer.flush();
                    log.info("Streamed file [{}] to client", filePath.getFileName());
                }

                // Write closing boundary
                writer.write("--" + BOUNDARY + "--\r\n");
                writer.flush();
            }
        };
    }
}

This controller streams two files sequentially as parts of a multipart/mixed response. Each part begins with the dynamic boundary BOUNDARY and includes the content disposition and type headers. Files are read line by line using BufferedReader, and BufferedOutputStream with OutputStreamWriter ensures efficient, buffered writes to the client.

The closing boundary signals the end of the multipart response. This approach allows multiple files to be sent efficiently without loading all of them into memory at once.

2. Spring WebFlux (Reactive)

WebFlux is built for streaming and backpressure, and in this article, we demonstrate reactive streaming for multipart uploads and sequential multipart responses using the FilePart and Flux<DataBuffer> APIs.

2.1 Controller: Streaming Upload (WebFlux)

In reactive applications, Spring WebFlux provides a non-blocking approach to handle file uploads and downloads. Instead of blocking threads while waiting for file I/O, WebFlux streams each part as it arrives, making it ideal for large files or high-concurrency scenarios where traditional blocking I/O could hinder scalability. Using FilePart.transferTo(Path), files are streamed directly to the target location without buffering the entire content in memory.

@Controller
public class WebFluxStreamingUploadController {

    private static final Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public Mono<String> streamFileUpload(@RequestPart("file") FilePart filePart) {
        return Mono.fromCallable(() -> {
            Files.createDirectories(UPLOAD_ROOT);
            return UPLOAD_ROOT.resolve(filePart.filename());
        }).flatMap(targetPath
                -> filePart.transferTo(targetPath)
                        .thenReturn("Upload successful: " + filePart.filename())
        );
    }
}

This controller defines a /upload endpoint for streaming file uploads using Spring WebFlux. The uploaded file is received as a FilePart, and the target directory webflux-uploads is created if it doesn’t exist. Using Mono.fromCallable, we determine the target path reactively, and filePart.transferTo(targetPath) streams the file content directly to disk in a non-blocking manner. The thenReturn operator allows returning a confirmation message once the transfer completes.

2.2 Controller: Streaming Multipart Download (WebFlux)

In WebFlux, streaming a multipart response is easiest by using a Flux<DataBuffer> as the response body, with the content type set to multipart/mixed; boundary=.... This method allows you to emit each part of the response, boundaries, headers, and file content, sequentially by reading the files with DataBufferUtils.read without loading everything into memory at once.

@RestController
public class WebFluxStreamingDownloadHandler {

    private static final Logger log = LoggerFactory.getLogger(WebFluxStreamingDownloadHandler.class);
    private final Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");
    private static final String BOUNDARY = "WebFluxBoundary_" + System.currentTimeMillis();
    private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();

    @GetMapping(path = "/download/multipart", produces = "multipart/mixed")
    public Mono<Void> streamMultipart(ServerHttpResponse response) {
        response.getHeaders().setContentType(MediaType.parseMediaType("multipart/mixed; boundary=" + BOUNDARY));

        Flux<DataBuffer> partsFlux;
        try {
            List<Path> files = Files.list(UPLOAD_ROOT).toList();
            partsFlux = Flux.fromIterable(files)
                    .concatMap(file -> {
                        String filename = file.getFileName().toString();
                        // headers for the part
                        String header = "--" + BOUNDARY + "\r\n"
                                + "Content-Type: application/octet-stream\r\n"
                                + "Content-Disposition: attachment; filename=\"" + filename + "\"\r\n\r\n";
                        DataBuffer headerBuf = bufferFactory.wrap(header.getBytes());
                        // content Flux<DataBuffer>
                        Flux<DataBuffer> content = DataBufferUtils.read(
                                file,
                                bufferFactory,
                                4096
                        );
                        // after content, produce CRLF
                        DataBuffer tail = bufferFactory.wrap("\r\n".getBytes());
                        return Flux.concat(Mono.just(headerBuf), content, Mono.just(tail));
                    })
                    // After all parts, produce closing boundary
                    .concatWith(Mono.just(bufferFactory.wrap(("--" + BOUNDARY + "--\r\n").getBytes())));
        } catch (Exception ex) {
            partsFlux = Flux.just(bufferFactory.wrap(("Error: " + ex.getMessage()).getBytes()));
        }

        return response.writeWith(partsFlux)
                .doOnError(e -> log.error("Streaming failed", e));
    }
}

We compute a Flux<DataBuffer> that, for each file, emits a header buffer, followed by the file content as a Flux<DataBuffer] using DataBufferUtils.read(InputStreamSupplier, factory, bufferSize), and then a trailing CRLF. concatMap preserves the sequential order of the parts. Finally, we append the closing boundary.

Using response.writeWith(partsFlux) writes the DataBuffer stream to the client as it becomes available, allowing the client to process earlier parts while the server continues reading the remaining files.

3. Conclusion

In this article, we explored how to efficiently stream multipart data in Spring using both Spring MVC and Spring WebFlux. We demonstrated sequential streaming for uploads and downloads, showing how Spring MVC handles file transfers using MultipartFile and StreamingResponseBody, and how WebFlux leverages FilePart and Flux<DataBuffer> for non-blocking, reactive streaming. By streaming data directly from the request or to the response, applications can handle large files without exhausting memory, improve performance, and support high-concurrency scenarios.

4. Download the Source Code

This article explored how to handle multipart data streaming in Spring.

Download
You can download the full source code of this example here: spring streaming multipart data

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button