Affects versions from 5.3.15 to 6.0.0-20220310.112646-346. I have only tested it with these two versions, but I suspect the problem to exist in all 5.x versions.
When a @RequestBody
-annotated argument fails to get resolved due to the an exception thrown in the constructor of the argument class, @ExceptionHandler
s in @RestControllerAdvice
s are not taken into account in WebFlux, whereas it works as expected in WebMVC.
Below, CustomWebFluxTest.test()
fails, whereas CustomWebMvcTest.test()
succeeds.
import com.fasterxml.jackson.annotation.JsonCreator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.*;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.BodyInserters;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
public class CtorFailureTest {
@Nested
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"spring.main.web-application-type=reactive"},
classes = {CustomConfiguration.class})
@ContextConfiguration(classes = CustomConfiguration.class)
class CustomWebFluxTest {
@Autowired
WebTestClient webTestClient;
@Test
void test() {
webTestClient
.post()
.uri("/custom")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue("{}"))
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
}
}
@Nested
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {CustomConfiguration.class})
class CustomWebMvcTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void test() {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE));
HttpEntity<String> requestEntity = new HttpEntity<>("{}", requestHeaders);
ResponseEntity<Void> responseEntity = restTemplate.exchange("/custom", HttpMethod.POST, requestEntity, Void.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
}
}
@Configuration
@EnableAutoConfiguration
@Import({CustomController.class, CustomAdvice.class})
static class CustomConfiguration {}
static final class CustomException extends RuntimeException {}
static final class CustomModel {
@JsonCreator
public CustomModel() {
throw new CustomException();
}
}
@RestController
static class CustomController {
@PostMapping(
path = "/custom",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.ACCEPTED)
void customResource(@RequestBody CustomModel ignored) {}
}
@RestControllerAdvice
static class CustomAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public void customHandler(CustomException ignored) {}
}
}
Comment From: rstoyanchev
Thanks for the sample code. CustomException
is a nested cause, more than a couple of levels deep. On the Spring MVC we unwrap all causes and provide those to the exception handler method. On the WebFlux side we only pass the first cause. It looks like we need to apply this change b587a16d460ad10a98874796c389b933ff85e457 on the WebFlux side too.