As explained in
Multipart Data
,
ServerWebExchange
provides access to multipart
content. The best way to handle a file upload form (for example, from a browser) in a controller
is through data binding to a
command object
,
as the following example shows:
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
// ...
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
"name": "value"
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
You can access individual parts with
@RequestPart
, as the following example shows:
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file) { (2)
// ...
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
You can use @RequestPart
in combination with jakarta.validation.Valid
or Spring’s
@Validated
annotation, which causes Standard Bean Validation to be applied. Validation
errors lead to a WebExchangeBindException
that results in a 400 (BAD_REQUEST) response.
The exception contains a BindingResult
with the error details and can also be handled
in the controller method by declaring the argument with an async wrapper and then using
error related operators:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
To access multipart data sequentially, in a streaming fashion, you can use @RequestBody
with
Flux<PartEvent>
(or Flow<PartEvent>
in Kotlin).
Each part in a multipart HTTP message will produce at
least one PartEvent
containing both headers and a buffer with the contents of the part.
File uploads will produce one or more FilePartEvent
objects, containing the filename used
when uploading. If the file is large enough to be split across multiple buffers, the first
FilePartEvent
will be followed by subsequent events.
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { (1)
allPartsEvents.windowUntil(PartEvent::isLast) (2)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> { (3)
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) { (4)
String value = formEvent.value();
// handle form field
else if (event instanceof FilePartEvent fileEvent) { (5)
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content); (6)
// handle file upload
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
else {
return partEvents; // either complete or error signal
The final PartEvent
for a particular part will have isLast()
set to true
, and can be
followed by additional events belonging to subsequent parts.
This makes the isLast
property suitable as a predicate for the Flux::windowUntil
operator, to
split events from all parts into windows that each belong to a single part.
The Flux::switchOnFirst
operator allows you to see whether you are handling a form field or
file upload.
Handling the form field.
Handling the file upload.
The body contents must be completely consumed, relayed, or released to avoid memory leaks.
@PostMapping("/")
fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { (1)
allPartsEvents.windowUntil(PartEvent::isLast) (2)
.concatMap {
it.switchOnFirst { signal, partEvents -> (3)
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) { (4)
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) { (5)
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); (6)
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
} else {
return partEvents; // either complete or error signal
The final PartEvent
for a particular part will have isLast()
set to true
, and can be
followed by additional events belonging to subsequent parts.
This makes the isLast
property suitable as a predicate for the Flux::windowUntil
operator, to
split events from all parts into windows that each belong to a single part.
The Flux::switchOnFirst
operator allows you to see whether you are handling a form field or
file upload.
Handling the form field.
Handling the file upload.
The body contents must be completely consumed, relayed, or released to avoid memory leaks.
© VMware, Inc. or its affiliates. Terms of Use • Privacy • Trademark Guidelines • Thank you • Your California Privacy Rights • Cookie Settings
Apache®, Apache Tomcat®, Apache Kafka®, Apache Cassandra™, and Apache Geode™ are trademarks or registered trademarks of the Apache Software Foundation in the United States and/or other countries. Java™, Java™ SE, Java™ EE, and OpenJDK™ are trademarks of Oracle and/or its affiliates. Kubernetes® is a registered trademark of the Linux Foundation in the United States and other countries. Linux® is the registered trademark of Linus Torvalds in the United States and other countries. Windows® and Microsoft® Azure are registered trademarks of Microsoft Corporation. “AWS” and “Amazon Web Services” are trademarks or registered trademarks of Amazon.com Inc. or its affiliates. All other trademarks and copyrights are property of their respective owners and are only mentioned for informative purposes. Other names may be trademarks of their respective owners.