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.

Comment From: mdeinum

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.

Comment From: snicoll

Thanks @mdeinum

@ozooxo see also the documentation.

Comment From: ozooxo

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 https://github.com/spring-projects/spring-framework/blob/abcad5dbcf139f316f8fdee129eee6ca9d40b39d/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java#L53 instead of MethodArgumentNotValidException https://github.com/spring-projects/spring-framework/blob/abcad5dbcf139f316f8fdee129eee6ca9d40b39d/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java#L60

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 https://github.com/spring-projects/spring-framework/blob/abcad5dbcf139f316f8fdee129eee6ca9d40b39d/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java#L131 is already defined in ResponseEntityExceptionHandler, for me to define another @ExceptionHandler(WebExchangeBindException::class) will cause a conflict.

Add another individual @RestControllerAdvice https://github.com/ozooxo/spring-jakarta-controller-advise-bug/commit/fa54a81b6b4fc9d1ebc2407d3b8fd1e33e02aabb will work.

@RestControllerAdvice
class AdditionalHandler {

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