Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request body deserialization failures are not captured by exception handlers in WebFlux #28155

Closed
vy opened this issue Mar 10, 2022 · 1 comment
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@vy
Copy link

vy commented Mar 10, 2022

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) {}

    }

}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 10, 2022
@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Mar 10, 2022
@rstoyanchev
Copy link
Contributor

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 b587a16 on the WebFlux side too.

@rstoyanchev rstoyanchev added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 25, 2022
@rstoyanchev rstoyanchev self-assigned this Apr 25, 2022
@rstoyanchev rstoyanchev added this to the 5.3.20 milestone Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants