Multipart Content

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:

  • Java

  • Kotlin

class MyForm {

	private String name;

	private MultipartFile file;

	// ...

}

@Controller
public class FileUploadController {

	@PostMapping("/form")
	public String handleFormUpload(MyForm form, BindingResult errors) {
		// ...
	}

}
class MyForm(
		val name: String,
		val file: MultipartFile)

@Controller
class FileUploadController {

	@PostMapping("/form")
	fun handleFormUpload(form: MyForm, errors: BindingResult): String {
		// ...
	}

}

You can also submit multipart requests from non-browser clients in a RESTful service scenario. The following example uses a file along with JSON:

POST /someUrl
Content-Type: multipart/mixed

--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:

  • Java

  • Kotlin

@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file) { (2)
	// ...
}
1 Using @RequestPart to get the metadata.
2 Using @RequestPart to get the file.
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file): String { (2)
	// ...
}
1 Using @RequestPart to get the metadata.
2 Using @RequestPart to get the file.

To deserialize the raw part content (for example, to JSON — similar to @RequestBody), you can declare a concrete target Object, instead of Part, as the following example shows:

  • Java

  • Kotlin

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
	// ...
}
1 Using @RequestPart to get the metadata.
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
	// ...
}
1 Using @RequestPart to get the metadata.

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:

  • Java

  • Kotlin

@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 {
	// ...
}

If method validation applies because other parameters have @Constraint annotations, then HandlerMethodValidationException is raised instead. See the section on Validation.

To access all multipart data as a MultiValueMap, you can use @RequestBody, as the following example shows:

  • Java

  • Kotlin

@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
	// ...
}
1 Using @RequestBody.
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
	// ...
}
1 Using @RequestBody.

PartEvent

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.

  • Form fields will produce a single FormPartEvent, containing the value of the field.

  • 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.

For example:

  • Java

  • Kotlin

@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
                }
            }));
}
1 Using @RequestBody.
2 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.
3 The Flux::switchOnFirst operator allows you to see whether you are handling a form field or file upload.
4 Handling the form field.
5 Handling the file upload.
6 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
                  }
              }
          }
}
1 Using @RequestBody.
2 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.
3 The Flux::switchOnFirst operator allows you to see whether you are handling a form field or file upload.
4 Handling the form field.
5 Handling the file upload.
6 The body contents must be completely consumed, relayed, or released to avoid memory leaks.

Received part events can also be relayed to another service by using the WebClient. See Multipart Data.