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 !!
Comments