Ralph Schaer opened SPR-17463 and commented
In a Spring Boot application with reactive web I have a POST endpoint with a request body of type Mono. This works fine with Spring Boot 2.0.6 (Spring 5.0.10). But when I upgraded to Spring Boot 2.1.0 (Spring 5.1.2) the application throws an error "Request body is missing"
To reproduce the problem create a Spring Boot 2.1.0 application with Spring Initializr and add "Reactive Web" as sole dependency.
Then add a PostMapping method to the main class
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@PostMapping("/register")
public void register(@RequestBody Mono<String> token) {
token.subscribe(System.out::println);
}
}
When you call this endpoint with curl the application throws an error
curl -v -d "token" -H "Content-Type: text/plain" http://localhost:8080/register
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.server.ServerWebInputException: 400 BAD_REQUEST "Request body is missing: public void com.example.demo.DemoApplication.register(reactor.core.publisher.Mono<java.lang.String>)"
Caused by: org.springframework.web.server.ServerWebInputException: 400 BAD_REQUEST "Request body is missing: public void com.example.demo.DemoApplication.register(reactor.core.publisher.Mono<java.lang.String>)"
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.handleMissingBody(AbstractMessageReaderArgumentResolver.java:223) ~[spring-webflux-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.lambda$readBody$5(AbstractMessageReaderArgumentResolver.java:188) ~[spring-webflux-5.1.2.RELEASE.jar:5.1.2.RELEASE]
When you change the Spring Boot dependency to 2.0.6.RELEASE and run the same curl command you see that it works without any problems.
Affects: 5.1.2
Comment From: spring-projects-issues
Brian Clozel commented
Hi Ralph Schaer,
I'm not sure we should consider that as a regression as I believe this case was never meant to work. The reference documentation says:
A method with a
void
, possibly asynchronous (for example,Mono<Void>
), return type (or anull
return value) is considered to have fully handled the response if it also has aServerHttpResponse
, aServerWebExchange
argument, or an@ResponseStatus
annotation.
Generally, a void
method return type means that by the time the controller method returns, the request/response exchange should be considered as processed. This means that WebFlux will close and release all HTTP resources. In your case, the incoming request is closed while the controller is still busy reading the incoming data.
I don't really know why this case was working previously, but maybe even in Spring Boot 2.0.6 a larger request body would break already.
A proper version of that Controller method would be:
@RestController
public class TestController {
@PostMapping("/register")
public Mono<Void> register(@RequestBody Mono<String> token) {
return token.doOnNext(System.out::println).then();
}
}
Now I don't know if we can clarify that part of the reference documentation even more; we could try and proactively reject those controller signatures, but it's not really something Spring usually does.
Comment From: spring-projects-issues
Ralph Schaer commented
Thanks a lot for the clarification. I changed the code with the proper implementation and it works fine now.
Comment From: spring-projects-issues
Rossen Stoyanchev commented
From a client perspective it is still a 200 response in both cases, but on the server side, as Brian pointed out, the void
return value indicates handling is complete too soon, and there may not be enough time to read body. I confirmed the same behavior with 2.0.6 by increasing the body content.
Comment From: pkgonan
@rstoyanchev @ralscha Hi. How did you solve this problem? I have same issue in spring framework 5.3.9 version.
Unable to execute HTTP request: 400 BAD_REQUEST "Request body is missing: public java.lang.Object
@PutMapping(value = ["/datas/{id}"])
suspend fun create(
@PathVariable @NotBlank id: String,
@RequestBody data: Flow<ByteBuffer>
): ResponseEntity<Void> {
val response = service.create(id, data)
val resource = response.resource
return ResponseEntity.ok()
.header(HttpHeaders.ETAG, resource.eTag)
.build()
}
Comment From: ralscha
@pkgonan I fixed it by using the correct return type. From public void register
to public Mono<Void> register