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, @ExceptionHandlers in @RestControllerAdvices 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.