1

Problem

We're developing a Spring Boot service to upload data to different back end databases. The idea is that, in one multipart/form-data request a user will send a "model" (basically a file) and "modelMetadata" (which is JSON that defines an object of the same name in our code).

We got the below to work in the WebFlux annotated controller syntax, when the user sends the "modelMetadata" in the multipart form with the content-type of "application/json":

    @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
    fun saveModel(@RequestPart("modelMetadata") monoModelMetadata: Mono<ModelMetadata>,
                  @RequestPart("model") monoModel: Mono<FilePart>,
                  @RequestHeader headers: HttpHeaders) : Mono<ResponseEntity<ModelMetadata>> {
        return modelService.saveModel(monoModelMetadata, monoModel, headers)
    }

But we can't seem to figure out how to do the same thing in Webflux's functional router definition. Below are the relevant code snippets we have:

    @Bean
    fun modelRouter() = router {
        accept(MediaType.MULTIPART_FORM_DATA).nest {
            POST(ROOT, handler::saveModel)
        }
    }


    fun saveModel(r: ServerRequest): Mono<ServerResponse> {
        val headers = r.headers().asHttpHeaders()
        val monoModelPart = r.multipartData().map { multiValueMap ->
            it["model"] // What do we do with this List<Part!> to get a Mono<FilePart>
            it["modelMetadata"] // What do we do with this List<Part!> to get a Mono<ModelMetadata>
        }

From everything we've read, we should be able to replicate the same functionality found in the annotation controller syntax with the router functional syntax, but this particular aspect doesn't seem to be well documented. Our goal was to move over to use the new functional router syntax since this is a new application we're developing and there are some nice forward thinking features/benefits as described here.

What we've tried

  • Googling to the ends of the Earth for a relevant example
    • this is a similar question, but hasn't gained any traction and doesn't relate to our need to create an object from one piece of the multipart request data
    • this may be close to what we need for uploading the file component of our multipart request data, but doesn't handle the object creation from JSON
  • Tried looking at the @RequestPart annotation code to see how things are done on that side, there's a nice comment that seems to hint at how they are converting the parts to objects, but we weren't able to figure out where that code lives or any relevant example of how to use an HttpMessageConverter on the ``
the content of the part is passed through an {@link HttpMessageConverter} taking into consideration the 'Content-Type' header of the request part.

Any and all help would be appreciated! Even just some links for us to better understand Part/FilePart types and there role in multipart requests would be helpful!

Alan Yeung
  • 180
  • 3
  • 7

1 Answers1

2

I was able to come up with a solution to this issue using an autowired ObjectMapper. From the below solution I could turn the modelMetadata and modelPart into Monos to mirror the @RequestPart return types, but that seems ridiculous.

I was also able to solve this by creating a MappingJackson2HttpMessageConverter and turning the metadataDataBuffer into a MappingJacksonInputMessage, but this solution seemed better for our needs.

    fun saveModel(r: ServerRequest): Mono<ServerResponse> {
        val headers = r.headers().asHttpHeaders()
        return r.multipartData().flatMap {
            // We're only expecting one Part of each to come through...assuming we understand what these Parts are
            if (it.getOrDefault("modelMetadata", listOf()).size == 1 && it.getOrDefault("model", listOf()).size == 1) {
                val modelMetadataPart = it["modelMetadata"]!![0]
                val modelPart = it["model"]!![0] as FilePart
                modelMetadataPart
                        .content()
                        .map { metadataDataBuffer ->
                            // TODO: Only do this if the content is JSON?
                            objectMapper.readValue(metadataDataBuffer.asInputStream(), ModelMetadata::class.java)
                        }
                        .next() // We're only expecting one object to be serialized from the buffer
                        .flatMap { modelMetadata ->
                            // Function was updated to work without needing the Mono's of each type 
                            // since we're mapping here
                            modelService.saveModel(modelMetadata, modelPart, headers)
                        }
            }
            else {
                    // Send bad request response message
            }
        }

Although this solution works, I feel like it's not as elegant as the one alluded to in the @RequestPart annotation comments. Thus I will accept this as the solution for now, but if someone has a better solution please let us know and I will accept it!

Alan Yeung
  • 180
  • 3
  • 7