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/
- First commit is generated by the Spring Starter
- In the second commit https://github.com/ozooxo/spring-jakarta-controller-advise-bug/commit/46015ba4467fbd47ec838015bdc579f8c55380e8 I added the Jakarta validation in the controller and service layer. I also overrides
ConstraintViolationException
to raise 401 instead of 400 with error messageoverride to bad bad bad
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"
)
}
}