Skip to content

ConstraintViolationExceptionHandler @ControllerAdvice doesn't work in the controller layer validation #33133

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

Closed
ozooxo opened this issue Jul 1, 2024 · 3 comments
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: invalid An issue that we don't feel is valid

Comments

@ozooxo
Copy link
Contributor

ozooxo commented Jul 1, 2024

It looks for me that the ConstraintViolationExceptionHandler with @ControllerAdvice doesn't work in the controller layer validation. It works for the service layer validation though.

See demo code in https://github.com/ozooxo/spring-jakarta-controller-advise-bug/

Run the application by ./gradlew bootRun.

Call the endpoint with service layer validation. The return is as expected.

curl -v http://localhost:8080/validation_error_raised_from_service -X POST
...
< HTTP/1.1 401 Unauthorized
...
{"type":"about:blank","title":"Unauthorized","status":401,"detail":"override to bad bad bad","instance":"/validation_error_raised_from_service"}%

However, if I call the controller layer validation, I got

curl -v http://localhost:8080/validation_error_raised_from_controller \
  -X POST \
  -d '{"name": "string_to_long"}' \
  -H "Content-Type: application/json"

< HTTP/1.1 400 Bad Request
...
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/validation_error_raised_from_controller"}%

The expected behavior is I get

< HTTP/1.1 401 Unauthorized
...
{"type":"about:blank","title":"Unauthorized","status":401,"detail":"override to bad bad bad","instance":"/validation_error_raised_from_service"}%

instead, because I have advised the controller to use 401 when it sees ConstraintViolationException.

Also, if I comment out this line and

curl -v http://localhost:8080/validation_error_raised_from_controller \
  -X POST \
  -d '{"name": "string_too_long"}' \
  -H "Content-Type: application/json"
...
< HTTP/1.1 200 OK
...
success%

Which indicates my controller endpoint is setting up correctly.

@ozooxo ozooxo changed the title ConstraintViolationExceptionHandler @ControllerAdvice doesn't work in the controller layer ConstraintViolationExceptionHandler @ControllerAdvice doesn't work in the controller layer validation Jul 1, 2024
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 1, 2024
@jhoeller jhoeller added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Jul 2, 2024
@mdeinum
Copy link
Contributor

mdeinum commented Jul 3, 2024

The controller and service are both handled by different components. The service is proxied by the MethodValidationPostProcessor which will call the validator before calling the actual method and by default will re-throw the ConstraintViolationException as is.

For the controller however this is not the case and validation is called as part of the regular web method execution and done as part of data binding. Historically this threw a MethodArgumentNotValidException when using @ModelAttribute and the ConstraintViolationException when directly using the JSR-303 annotations on the method arguments (like @NotNull). In newer Spring versions this changed and for a controller both situation will now throw a HandlerMethodValidationException. If you don't it will be handled by the ResponseStatusExceptionHandler which will transform it based on the information available on the HandlerMethodValidationException (which is a ResponseStatusException) and this will produce the HTTP 400.

You would need to handle the HandlerMethodValidationException in your @ControllerAdvice as well.

@snicoll
Copy link
Member

snicoll commented Jul 3, 2024

Thanks @mdeinum

@ozooxo see also the documentation.

@snicoll snicoll closed this as not planned Won't fix, can't repro, duplicate, stale Jul 3, 2024
@snicoll snicoll added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jul 3, 2024
@ozooxo
Copy link
Contributor Author

ozooxo commented Jul 3, 2024

I think I worked it out. The story is slightly different.

First, since my application is using spring-webflux (not spring-mvc), the default message Invalid request content I received from

{"type":"about:blank","title":"Bad Request","status":400,"detail":"Invalid request content.","instance":"/validation_error_raised_from_controller"}%

is in WebExchangeBindException

instead of MethodArgumentNotValidException
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content.");

Second, to further handle WebExchangeBindException, I shouldn't handle it by extending ResponseEntityExceptionHandler. By doing so, I can no longer start my application. The error is

./gradlew bootRun
...
Caused by: java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.support.WebExchangeBindException]: {protected org.springframework.http.ProblemDetail com.example.demo.AdditionalHandler.handleException(org.springframework.web.bind.support.WebExchangeBindException), public final reactor.core.publisher.Mono org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.server.ServerWebExchange)}

which is saying that WebExchangeBindException is a subclass of ErrorResponseException , and since

is already defined in ResponseEntityExceptionHandler, for me to define another @ExceptionHandler(WebExchangeBindException::class) will cause a conflict.

Add another individual @RestControllerAdvice ozooxo/spring-jakarta-controller-advise-bug@fa54a81 will work.

@RestControllerAdvice
class AdditionalHandler {

    @ExceptionHandler(WebExchangeBindException::class)
    protected fun handleException(
        ex: WebExchangeBindException
    ): ProblemDetail {
        return ProblemDetail.forStatusAndDetail(
            HttpStatus.UNAUTHORIZED,
            "override to worse worse worse"
        )
    }
}

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) status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

5 participants