Add default web handling of method validation errors

Closes gh-30644
This commit is contained in:
rstoyanchev
2023-06-30 16:09:33 +01:00
parent a481c7649f
commit 7a79da589a
10 changed files with 339 additions and 63 deletions

View File

@@ -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

View File

@@ -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