fun createPost(request: ServerRequest): Mono<ServerResponse> =
    with(request) {
        //            body(BodyExtractors.toMultipartData()) // Working
        multipartData() // throw DecodingException("Could not find first boundary")
            .map { CreatePostRequest(it) }
            .zipWith(getAuthentication())
            .flatMap { (createPostRequest, authentication) ->
                ServerResponse.ok()
                    .body(postService.createPost(createPostRequest, authentication))
            }
    }

I was trying to handle multipart/form-data formatted requests in a functional endpoint in Spring WebFlux.

2024-06-04 17:36:55.110 INFO [reactor-http-nio-2] c.d.c.l.LoggingFilter: [cc18c85e] HTTP POST /post --8d-Ft4MJuxLL.EX84HMpEjpVRztZQguETCWv61okwhO3dj3rGSdZviiXNOLQPVJ9Um3Pr2 content-disposition: form-data; name="boardId" 662f548bc333f951251ea702 --8d-Ft4MJuxLL.EX84HMpEjpVRztZQguETCWv61okwhO3dj3rGSdZviiXNOLQPVJ9Um3Pr2 content-disposition: form-data; name="title" As --8d-Ft4MJuxLL.EX84HMpEjpVRztZQguETCWv61okwhO3dj3rGSdZviiXNOLQPVJ9Um3Pr2 content-disposition: form-data; name="content" Da --8d-Ft4MJuxLL.EX84HMpEjpVRztZQguETCWv61okwhO3dj3rGSdZviiXNOLQPVJ9Um3Pr2-- 
2024-06-04 17:36:55.131 ERROR [reactor-http-nio-2] c.d.c.e.GlobalExceptionHandler: [cc18c85e] DecodingException("Could not find first boundary") at org.springframework.http.codec.multipart.MultipartParser$PreambleState.onComplete(MultipartParser.java:339)
2024-06-04 17:36:55.137 INFO [reactor-http-nio-2] c.d.c.l.LoggingFilter: [cc18c85e] HTTP 500 INTERNAL_SERVER_ERROR

However, unlike body(BodyExtractors.toMultipartData()), we confirmed that multipartData() fails with DecodingException("Could not find first boundary").

Strangely enough, the cause was WebFliter, which was being used for logging.

class LoggingFilter : WebFilter {
    private val logger = getLogger()

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> =
        exchange.log()
            .flatMap { chain.filter(it) }

    private fun ServerWebExchange.log(): Mono<ServerWebExchange> =
        request.body
            .toByteArray() // Method to convert Flux<DataBuffer> to Mono<ByteArray> via DataBufferUtils
            .defaultIfEmpty(ByteArray(0)) // Implemented to return an empty ByteArray if there is no Request Body
            .doOnNext { loggingRequest(request, String(it)) }
            .map {
                mutate()
                    .request(object : ServerHttpRequestDecorator(request) {
                        override fun getBody(): Flux<DataBuffer> =
                            Flux.just(
                                response.bufferFactory()
                                    .wrap(it)
                            )
                    })
                    .response(response.apply {
                        beforeCommit {
                            loggingResponse(this)
                            Mono.empty()
                        }
                    })
                    .build()
            }

    private fun loggingRequest(request: ServerHttpRequest, body: String) {
        request.apply {
            logger.info {
                "HTTP $method ${uri.run { "$path${query?.let { "?$it" } ?: ""}" }} ${body.toPrettyJson()}"
            }
        }
    }

    private fun loggingResponse(response: ServerHttpResponse) {
        logger.info { "HTTP ${response.statusCode}" }
    }

The code above is the WebFilter used for logging earlier multipartData() works fine if I remove the part that reads the Flux<DataBuffer> from the LoggingFilter. I also verified that the contents of the Flux<DataBuffer> read from the LoggingFilter and the Flux<DataBuffer> read from the Handler that went through the LoggingFilter are the same. When using multipartData(), is it intentional that WebFilter cannot access the body of multipart/form-data?

Comment From: sdeleuze

Could you please provide a self-contained reproducer (attached archive or link to a repository)?

Comment From: earlgrey02

I have temporarily changed the repository to public. Currently, LoggingFilter has been modified to not read the request body of multipart/form-data. If you delete this part, you can reproduce the error. multipartData() is used in the following handler: PostHandler.kt

Comment From: sdeleuze

I cloned to repository but I get an error while running ./gradlew build. Please provide a minimal reproducer and step by step instructions on how to reproduce.

Comment From: earlgrey02

I cloned to repository but I get an error while running ./gradlew build. Please provide a minimal reproducer and step by step instructions on how to reproduce.

Sorry for not checking properly. I created a new repository. For convenience, I have created "/test-failure" and "/test-success" endpoints that send requests with mock formData to APIs that use multipartData() and body(BodyExtractors.toMultipartData()). The exception occurs only on API using multipartData(). To avoid exception, remove the part that reads Flux<DataBuffer> from MultipartFilter.

The process of re-implementing the problem in the repository is as follows.

  1. Register a WebFilter that reads the request body through Flux<DataBuffer> in the ApplicationContext.
  2. Using multipartData() in Spring WebFlux’s functional endpoint.

Comment From: rstoyanchev

@earlgrey02 we don't really expect the body to be read twice in a multipart scenario. The multipart method in DefaultServerWebExchange is initialized up front based on the original (and not mutated) request. Parts can be binary and not printable, or very large so this is not something we want to encourage in general.

That said, the Mono returned from getMultipartData() method involves the cache operator. Have you tried using that for logging from the filter? It would also allow you to check the content-type and content-length of each part to decide whether to log.

Comment From: earlgrey02

As you suggested, I confirmed that using getMultipartData() instead of request.body in WebFilter does not throw an exception. But I don't understand why reading Flux<DataBuffer> only affects multipartData() and not BodyExtractors.multipartData(). I'm curious what you think about bodyExtractors and the need to have consistent behavior and results with things like multipartData() or formData() etc.

Comment From: rstoyanchev

That's what I was alluding to with:

The multipart method in DefaultServerWebExchange is initialized up front based on the original (and not mutated) request.

My suggestion would be to use the designated method for access to multipart data rather than going directly to the response body.

Comment From: earlgrey02

Thank you for your quick reply. I understand the proposal well, but I just have the following questions.

I'm curious what you think about bodyExtractors and the need to have consistent behavior and results with things like multipartData() or formData() etc.