Spring Boot – PartEvent API to Stream Multipart Form Uploads

Since Spring 6 and Spring boot 3, we can use the new PartEvent API for streaming multipart events to a Spring WebFlux controller. The PartEvent API helps in handling the multipart data sequentially, in a streaming fashion.

1. Introduction

When using part events, each part in a multipart HTTP message will produce at least one PartEvent instance containing both headers and a buffer with the contents of the part.

The multipart content can be sent through PartEvent objects:

  • FormPartEvent: a single object is produced for each form field, containing the field’s value.
  • FilePartEvent: one or more objects are produced containing the filename and content. If the file is large enough to be split across multiple buffers, the first FilePartEvent will be followed by subsequent events.

We can use @RequestBody with Flux (or Flow in Kotlin) for accepting the multiparts on the server side.

2. Multipart File Upload Controller

The following is an example of a file upload controller and its handler method accepting multipart events.

  • It uses PartEvent::isLast is true if there are additional events belonging to subsequent parts.
  • The Flux::switchOnFirst operator allows seeing whether we are handling a form field or file upload.
  • We can use the respective methods such as FormPartEvent.value(), FilePartEvent.filename() and PartEvent::content for retriving the information from multipart upload parts.

Note that the body contents must be completely consumed, relayed, or released to avoid memory leaks.

import static org.springframework.http.ResponseEntity.ok;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@Slf4j
public class FileUploadController {

  @PostMapping("upload-with-part-events")
  public ResponseEntity<Flux<String>> handlePartsEvents(@RequestBody Flux<PartEvent> allPartsEvents) {

    var result = allPartsEvents.windowUntil(PartEvent::isLast)
        .concatMap(p -> {
              return p.switchOnFirst((signal, partEvents) -> {
                    if (signal.hasValue()) {
                      PartEvent event = signal.get();
                      if (event instanceof FormPartEvent formEvent) {

                        String value = formEvent.value();
                        log.info("form value: {}", value);

                        // handle form field
                        return Mono.just(value + "\n");
                      } else if (event instanceof FilePartEvent fileEvent) {

                        String filename = fileEvent.filename();
                        log.info("upload file name:{}", filename);

                        // handle file upload
                        Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
                        var fileBytes = DataBufferUtils.join(contents)
                            .map(dataBuffer -> {
                              byte[] bytes = new byte[dataBuffer.readableByteCount()];
                              dataBuffer.read(bytes);
                              DataBufferUtils.release(dataBuffer);
                              return bytes;
                            });

                        return Mono.just(filename);
                      } else 
                      // no value
                      return Mono.error(new RuntimeException("Unexpected event: " + event));
                    }

                    log.info("return default flux");
                    //return result;
                    return Flux.empty(); // either complete or error signal
                  }
              );
            }
        );

    return ok().body(result);
  }
}

3. Sending PartEvent objects using WebTestClient

We have created a @WebFluxTest annotated test class that auto-configures a WebTestClient. We will use the WebTestClient to send the multipart part events to the above file upload API and verify the result.

Do not forget to put the ‘spring.png’ in the ‘/resources’ folder else you may get FileNotFoundException.

import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import com.howtodoinjava.app.web.FileUploadController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;

@WebFluxTest
public class FileUploadControllerTest {

  @Autowired
  FileUploadController fileUploadController;

  @Autowired
  WebTestClient client;

  @Test
  public void testUploadUsingPartEvents() {

    this.client
        .post().uri("/upload-with-part-events")
        .contentType(MULTIPART_FORM_DATA)
        .body(
            Flux.concat(
                FormPartEvent.create("name", "test"),
                FilePartEvent.create("file", new ClassPathResource("spring.png"))
            ),
            PartEvent.class
        )
        .exchange()
        .expectStatus().isOk()
        .expectBodyList(String.class).hasSize(2);
  }
}

The test passes successfully, and we can verify the results in the console logs.

...c.h.app.web.FileUploadController : form value: test
...c.h.app.web.FileUploadController : upload file name:spring.png

4. Conclusion

In this short tutorial, we learned to use the newly introduced PartEvent API in Spring 6 for sending multipart requests to a webflux controller and handling the uploaded form parameters and file contents.

Happy Learning !!

Sourcecode on Github

Leave a Comment

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode