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.
- Register a
WebFilter
that reads the request body throughFlux<DataBuffer>
in theApplicationContext
. - 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 likemultipartData()
orformData()
etc.