Add default web handling of method validation errors
Closes gh-30644
This commit is contained in:
@@ -34,6 +34,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
@@ -48,6 +49,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
@@ -121,6 +123,7 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
MissingServletRequestPartException.class,
|
||||
ServletRequestBindingException.class,
|
||||
MethodArgumentNotValidException.class,
|
||||
HandlerMethodValidationException.class,
|
||||
NoHandlerFoundException.class,
|
||||
NoResourceFoundException.class,
|
||||
AsyncRequestTimeoutException.class,
|
||||
@@ -129,6 +132,7 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
TypeMismatchException.class,
|
||||
HttpMessageNotReadableException.class,
|
||||
HttpMessageNotWritableException.class,
|
||||
MethodValidationException.class,
|
||||
BindException.class
|
||||
})
|
||||
@Nullable
|
||||
@@ -157,6 +161,9 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
else if (ex instanceof MethodArgumentNotValidException subEx) {
|
||||
return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
|
||||
}
|
||||
else if (ex instanceof HandlerMethodValidationException subEx) {
|
||||
return handleHandlerMethodValidationException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
|
||||
}
|
||||
else if (ex instanceof NoHandlerFoundException subEx) {
|
||||
return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
|
||||
}
|
||||
@@ -185,6 +192,9 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
else if (ex instanceof HttpMessageNotWritableException theEx) {
|
||||
return handleHttpMessageNotWritable(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
|
||||
}
|
||||
else if (ex instanceof MethodValidationException subEx) {
|
||||
return handleMethodValidationException(subEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
|
||||
}
|
||||
else if (ex instanceof BindException theEx) {
|
||||
return handleBindException(theEx, headers, HttpStatus.BAD_REQUEST, request);
|
||||
}
|
||||
@@ -335,6 +345,24 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
return handleExceptionInternal(ex, null, headers, status, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the handling of {@link HandlerMethodValidationException}.
|
||||
* <p>This method delegates to {@link #handleExceptionInternal}.
|
||||
* @param ex the exception to handle
|
||||
* @param headers the headers to be written to the response
|
||||
* @param status the selected response status
|
||||
* @param request the current request
|
||||
* @return a {@code ResponseEntity} for the response to use, possibly
|
||||
* {@code null} when the response is already committed
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
protected ResponseEntity<Object> handleHandlerMethodValidationException(
|
||||
HandlerMethodValidationException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
|
||||
|
||||
return handleExceptionInternal(ex, null, headers, status, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the handling of {@link NoHandlerFoundException}.
|
||||
* <p>This method delegates to {@link #handleExceptionInternal}.
|
||||
@@ -521,6 +549,29 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
|
||||
return handleExceptionInternal(ex, body, headers, status, request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Customize the handling of {@link MethodValidationException}.
|
||||
* <p>By default this method creates a {@link ProblemDetail} with the status
|
||||
* and a short detail message, and also looks up an override for the detail
|
||||
* via {@link MessageSource}, before delegating to
|
||||
* {@link #handleExceptionInternal}.
|
||||
* @param ex the exception to handle
|
||||
* @param headers the headers to use for the response
|
||||
* @param status the status code to use for the response
|
||||
* @param request the current request
|
||||
* @return a {@code ResponseEntity} for the response to use, possibly
|
||||
* {@code null} when the response is already committed
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
protected ResponseEntity<Object> handleMethodValidationException(
|
||||
MethodValidationException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||
|
||||
ProblemDetail body = createProblemDetail(ex, status, "Validation failed", null, null, request);
|
||||
return handleExceptionInternal(ex, body, headers, status, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to create a {@link ProblemDetail} for any exception
|
||||
* that doesn't implement {@link ErrorResponse}, also performing a
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
@@ -118,8 +120,8 @@ import org.springframework.web.util.WebUtils;
|
||||
* <td><div class="block">MethodArgumentNotValidException</div></td>
|
||||
* <td><div class="block">400 (SC_BAD_REQUEST)</div></td>
|
||||
* </tr>
|
||||
* <tr class="even-row-color">
|
||||
* <td><div class="block">BindException</div></td>
|
||||
* <tr class="odd-row-color">
|
||||
* <td><div class="block">{@link HandlerMethodValidationException}</div></td>
|
||||
* <td><div class="block">400 (SC_BAD_REQUEST)</div></td>
|
||||
* </tr>
|
||||
* <tr class="odd-row-color">
|
||||
@@ -134,6 +136,10 @@ import org.springframework.web.util.WebUtils;
|
||||
* <td><div class="block">AsyncRequestTimeoutException</div></td>
|
||||
* <td><div class="block">503 (SC_SERVICE_UNAVAILABLE)</div></td>
|
||||
* </tr>
|
||||
* <tr class="odd-row-color">
|
||||
* <td><div class="block">{@link MethodValidationException}</div></td>
|
||||
* <td><div class="block">500 (SC_INTERNAL_SERVER_ERROR)</div></td>
|
||||
* </tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
*
|
||||
@@ -200,6 +206,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
else if (ex instanceof MethodArgumentNotValidException theEx) {
|
||||
mav = handleMethodArgumentNotValidException(theEx, request, response, handler);
|
||||
}
|
||||
else if (ex instanceof HandlerMethodValidationException theEx) {
|
||||
mav = handleHandlerMethodValidationException(theEx, request, response, handler);
|
||||
}
|
||||
else if (ex instanceof NoHandlerFoundException theEx) {
|
||||
mav = handleNoHandlerFoundException(theEx, request, response, handler);
|
||||
}
|
||||
@@ -228,6 +237,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
else if (ex instanceof HttpMessageNotWritableException theEx) {
|
||||
return handleHttpMessageNotWritable(theEx, request, response, handler);
|
||||
}
|
||||
else if (ex instanceof MethodValidationException theEx) {
|
||||
return handleMethodValidationException(theEx, request, response, handler);
|
||||
}
|
||||
else if (ex instanceof BindException theEx) {
|
||||
return handleBindException(theEx, request, response, handler);
|
||||
}
|
||||
@@ -399,6 +411,26 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case where method validation for a controller method failed.
|
||||
* <p>The default implementation returns {@code null} in which case the
|
||||
* exception is handled in {@link #handleErrorResponse}.
|
||||
* @param ex the exception to be handled
|
||||
* @param request current HTTP request
|
||||
* @param response current HTTP response
|
||||
* @param handler the executed handler
|
||||
* @return an empty {@code ModelAndView} indicating the exception was handled, or
|
||||
* {@code null} indicating the exception should be handled in {@link #handleErrorResponse}
|
||||
* @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
|
||||
* @since 6.1
|
||||
*/
|
||||
@Nullable
|
||||
protected ModelAndView handleHandlerMethodValidationException(HandlerMethodValidationException ex,
|
||||
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case where no handler was found during the dispatch.
|
||||
* <p>The default implementation returns {@code null} in which case the
|
||||
@@ -577,6 +609,27 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
return new ModelAndView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case where method validation failed on a component that is
|
||||
* not a web controller, e.g. on some underlying service.
|
||||
* <p>The default implementation sends an HTTP 500 error, and returns an empty {@code ModelAndView}.
|
||||
* Alternatively, a fallback view could be chosen, or the HttpMessageNotWritableException could
|
||||
* be rethrown as-is.
|
||||
* @param ex the exception to be handled
|
||||
* @param request current HTTP request
|
||||
* @param response current HTTP response
|
||||
* @param handler the executed handler
|
||||
* @return an empty {@code ModelAndView} indicating the exception was handled
|
||||
* @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
|
||||
* @since 6.1
|
||||
*/
|
||||
protected ModelAndView handleMethodValidationException(MethodValidationException ex,
|
||||
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
|
||||
|
||||
sendServerError(ex, request, response);
|
||||
return new ModelAndView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case where an {@linkplain ModelAttribute @ModelAttribute} method
|
||||
* argument has binding or validation errors and is not followed by another
|
||||
|
||||
@@ -39,7 +39,6 @@ import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||
import org.springframework.validation.beanvalidation.ParameterValidationResult;
|
||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
@@ -164,9 +163,9 @@ public class MethodValidationTests {
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
this.request.addHeader("myHeader", "123");
|
||||
|
||||
MethodValidationException ex = catchThrowableOfType(
|
||||
HandlerMethodValidationException ex = catchThrowableOfType(
|
||||
() -> this.handlerAdapter.handle(this.request, this.response, hm),
|
||||
MethodValidationException.class);
|
||||
HandlerMethodValidationException.class);
|
||||
|
||||
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||
assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||
|
||||
@@ -43,6 +43,8 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.MapBindingResult;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationResult;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
@@ -58,6 +60,7 @@ import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
@@ -69,6 +72,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.testfixture.servlet.MockServletConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResponseEntityExceptionHandler}.
|
||||
@@ -245,6 +249,16 @@ public class ResponseEntityExceptionHandlerTests {
|
||||
new MapBindingResult(Collections.emptyMap(), "name")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handlerMethodValidationException() {
|
||||
testException(new HandlerMethodValidationException(mock(MethodValidationResult.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void methodValidationException() {
|
||||
testException(new MethodValidationException(mock(MethodValidationResult.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingServletRequestPart() {
|
||||
testException(new MissingServletRequestPartException("partName"));
|
||||
@@ -351,6 +365,7 @@ public class ResponseEntityExceptionHandlerTests {
|
||||
private ResponseEntity<Object> testException(Exception ex) {
|
||||
try {
|
||||
ResponseEntity<Object> entity = this.exceptionHandler.handleException(ex, this.request);
|
||||
assertThat(entity).isNotNull();
|
||||
|
||||
// SPR-9653
|
||||
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(entity.getStatusCode())) {
|
||||
@@ -383,7 +398,7 @@ public class ResponseEntityExceptionHandlerTests {
|
||||
private static class NestedExceptionThrowingController {
|
||||
|
||||
@RequestMapping("/")
|
||||
public void handleRequest() throws Exception {
|
||||
public void handleRequest() {
|
||||
throw new IllegalStateException(new ServletRequestBindingException("message"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user